Merge branch 'master' of github.com:nodemailer/wildduck

This commit is contained in:
Andris Reinman 2019-03-13 15:39:26 +02:00
commit 90a4603bd5
17 changed files with 214 additions and 125 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2019-02-05T18:57:01.438Z", "url": "http://apidocjs.com", "version": "0.17.7" } });
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2019-02-26T12:32:31.900Z", "url": "http://apidocjs.com", "version": "0.17.7" } });

View file

@ -1 +1 @@
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2019-02-05T18:57:01.438Z", "url": "http://apidocjs.com", "version": "0.17.7" } }
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2019-02-26T12:32:31.900Z", "url": "http://apidocjs.com", "version": "0.17.7" } }

View file

@ -37,7 +37,7 @@ function send() {
from: 'Kärbes 🐧 <andris@kreata.ee>',
to: recipients
.map((rcpt, i) => ({ name: 'Recipient #' + (i + 1), address: rcpt }))
.map((rcpt) => ({ name: rcpt.split('@')[0], address: rcpt }))
.concat('andris <andris.reinman@gmail.com>, andmekala <andmekala@hot.ee>'),
cc: '"Juulius Orro" muna@gmail.com, kixgraft@gmail.com',
subject: 'Test ööö message [' + Date.now() + ']',

View file

@ -13,7 +13,7 @@ const EventEmitter = require('events').EventEmitter;
const packageInfo = require('../../package');
const errors = require('../../lib/errors.js');
const SOCKET_TIMEOUT = 10 * 60 * 1000;
const SOCKET_TIMEOUT = 5 * 60 * 1000;
/**
* Creates a handler for new socket
@ -202,7 +202,40 @@ class IMAPConnection extends EventEmitter {
*/
send(payload, callback) {
if (this._socket && this._socket.writable) {
this[!this.compression ? '_socket' : '_deflate'].write(payload + '\r\n', 'binary', callback);
try {
this[!this.compression ? '_socket' : '_deflate'].write(payload + '\r\n', 'binary', (...args) => {
if (args[0]) {
// write error
this.logger.error(
{
tnx: 'send',
cid: this.id,
err: args[0]
},
'[%s] Send error: %s',
this.id,
args[0].message || args[0]
);
return this.close();
}
if (typeof callback === 'function') {
return callback(...args);
}
});
} catch (err) {
// write error
this.logger.error(
{
tnx: 'send',
cid: this.id,
err
},
'[%s] Send error: %s',
this.id,
err.message || err
);
return this.close();
}
if (this.compression) {
// make sure we transmit the message immediatelly
this._deflate.flush();
@ -216,6 +249,9 @@ class IMAPConnection extends EventEmitter {
this.id,
payload
);
} else {
// socket is not there anymore
this.close();
}
}
@ -223,12 +259,33 @@ class IMAPConnection extends EventEmitter {
* Close socket
*/
close(force) {
if (this._closed || this._closing) {
return;
}
if (!this._socket.destroyed && this._socket.writable) {
this._socket[!force ? 'end' : 'destroy']();
}
this._server.connections.delete(this);
if (!force) {
// allow socket to close in 1500ms or force it to close
this._closingTimeout = setTimeout(() => {
if (this._closed) {
return;
}
try {
this._socket.destroy();
} catch (err) {
// ignore
}
setImmediate(() => this._onClose());
}, 1500);
}
this._closing = true;
if (force) {
setImmediate(() => this._onClose());
@ -263,6 +320,8 @@ class IMAPConnection extends EventEmitter {
* @event
*/
_onClose(/* hadError */) {
clearTimeout(this._closingTimeout);
if (this._closed) {
return;
}
@ -401,7 +460,8 @@ class IMAPConnection extends EventEmitter {
if (this.idling) {
// see if the connection still works
this.send('* OK Still here');
this.send('* OK Still here (' + Date.now() + ')');
this._socket.setTimeout(this._server.options.socketTimeout || SOCKET_TIMEOUT, this._onTimeout.bind(this));
return;
}
@ -464,6 +524,12 @@ class IMAPConnection extends EventEmitter {
*/
setupNotificationListener() {
let conn = this;
if (this._closing || this._closed) {
// nothing to do here
return;
}
let isSelected = mailbox => mailbox && conn.selected && conn.selected.mailbox && conn.selected.mailbox.toString() === mailbox.toString();
this._listenerData = {
@ -471,6 +537,10 @@ class IMAPConnection extends EventEmitter {
cleared: false,
callback(message) {
let selectedMailbox = conn.selected && conn.selected.mailbox;
if (this._closing || this._closed) {
conn.clearNotificationListener();
return;
}
if (message) {
// global triggers
@ -791,14 +861,13 @@ class IMAPConnection extends EventEmitter {
switch (key) {
case 'FLAGS':
value = [].concat(value || []).map(
flag =>
flag && flag.value
? flag
: {
type: 'ATOM',
value: flag
}
value = [].concat(value || []).map(flag =>
flag && flag.value
? flag
: {
type: 'ATOM',
value: flag
}
);
break;

View file

@ -240,7 +240,7 @@ module.exports = done => {
let iPos = 0;
let startInterfaces = () => {
if (iPos >= ifaceOptions.length) {
return done();
return db.redis.del('lim:imap', () => done());
}
let opts = ifaceOptions[iPos++];

View file

@ -1955,7 +1955,7 @@ module.exports = (db, server, messageHandler, userHandler) => {
* @apiParam {String} reference.mailbox Mailbox ID
* @apiParam {Number} reference.id Message ID in Mailbox
* @apiParam {String} reference.action Either <code>reply</code>, <code>replyAll</code> or <code>forward</code>
* @apiParam {Boolean} reference.attachments=false If true, then also adds attachments from the original message
* @apiParam {String[]} reference.attachments=false If true, then includes all attachments from the original message. If it is an array of attachment ID's includes attachments from the list
* @apiParam {String} [sess] Session identifier for the logs
* @apiParam {String} [ip] IP address for the logs
*
@ -2144,10 +2144,18 @@ module.exports = (db, server, messageHandler, userHandler) => {
action: Joi.string()
.valid('reply', 'replyAll', 'forward')
.required(),
attachments: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.default(false)
attachments: Joi.alternatives().try(
Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, '']),
Joi.array()
.items(
Joi.string()
.regex(/^ATT\d+$/i)
.uppercase()
)
.allow([])
)
}),
sess: Joi.string().max(255),
@ -3566,6 +3574,10 @@ module.exports = (db, server, messageHandler, userHandler) => {
// skip embedded images
continue;
}
if (Array.isArray(options.reference.attachments) && !options.reference.attachments.includes(attachment.id)) {
// skip attachments not listed in the API call
continue;
}
try {
let attachmentId = messageData.mimeTree.attachmentMap && messageData.mimeTree.attachmentMap[attachment.id];

View file

@ -206,7 +206,11 @@ module.exports = (db, server, notifier) => {
req.connection.on('error', done);
};
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no'
});
if (lastEventId) {
loadJournalStream(db, req, res, user, lastEventId, (err, info) => {

View file

@ -352,6 +352,7 @@ module.exports = (db, server, userHandler) => {
* @apiParam {Number} [imapMaxUpload] How many bytes can be uploaded via IMAP during 24 hour
* @apiParam {Number} [imapMaxDownload] How many bytes can be downloaded via IMAP during 24 hour
* @apiParam {Number} [pop3MaxDownload] How many bytes can be downloaded via POP3 during 24 hour
* @apiParam {Number} [imapMaxConnections] How many parallel IMAP connections are alowed
* @apiParam {Number} [receivedMax] How many messages can be received from MX during 60 seconds
* @apiParam {Object} [mailboxes] Optional names for special mailboxes
* @apiParam {String} [mailboxes.sent="Sent Mail"] Path of Sent Mail folder
@ -486,6 +487,9 @@ module.exports = (db, server, userHandler) => {
pop3MaxDownload: Joi.number()
.min(0)
.default(0),
imapMaxConnections: Joi.number()
.min(0)
.default(0),
receivedMax: Joi.number()
.min(0)
.default(0),
@ -909,6 +913,8 @@ module.exports = (db, server, userHandler) => {
* @apiSuccess {Number} limits.pop3Download.allowed How many bytes per 24 hours can be downloaded via POP3. Only message contents are counted, not protocol overhead.
* @apiSuccess {Number} limits.pop3Download.used How many bytes are downloaded during current 24 hour period
* @apiSuccess {Number} limits.pop3Download.ttl Time until the end of current 24 hour period
* @apiSuccess {Number} limits.imapMaxConnections.allowed How many parallel IMAP connections are permitted
* @apiSuccess {Number} limits.imapMaxConnections.used How many parallel IMAP connections are currenlty in use
*
* @apiSuccess {String[]} tags List of tags associated with the User
* @apiSuccess {String[]} disabledScopes Disabled scopes for this user
@ -1062,6 +1068,8 @@ module.exports = (db, server, userHandler) => {
.get('pdw:' + userData._id.toString())
.ttl('pdw:' + userData._id.toString())
.hget('lim:imap', userData._id.toString())
.exec();
} catch (err) {
// ignore
@ -1089,6 +1097,8 @@ module.exports = (db, server, userHandler) => {
let pop3Download = Number(response && response[10] && response[10][1]) || 0;
let pop3DownloadTtl = Number(response && response[11] && response[11][1]) || 0;
let imapMaxConnections = Number(response && response[12] && response[12][1]) || 0;
let keyInfo;
try {
keyInfo = await getKeyInfo(userData.pubKey);
@ -1161,6 +1171,11 @@ module.exports = (db, server, userHandler) => {
allowed: Number(userData.pop3MaxDownload) || (config.pop3.maxDownloadMB || 10000) * 1024 * 1024,
used: pop3Download,
ttl: pop3DownloadTtl >= 0 ? pop3DownloadTtl : false
},
imapMaxConnections: {
allowed: Number(userData.imapMaxConnections) || config.imap.maxConnections,
used: imapMaxConnections
}
},
@ -1210,6 +1225,7 @@ module.exports = (db, server, userHandler) => {
* @apiParam {Number} [imapMaxUpload] How many bytes can be uploaded via IMAP during 24 hour
* @apiParam {Number} [imapMaxDownload] How many bytes can be downloaded via IMAP during 24 hour
* @apiParam {Number} [pop3MaxDownload] How many bytes can be downloaded via POP3 during 24 hour
* @apiParam {Number} [imapMaxConnections] How many parallel IMAP connections are alowed
* @apiParam {Number} [receivedMax] How many messages can be received from MX during 60 seconds
* @apiParam {Boolean} [disable2fa] If true, then disables 2FA for this user
* @apiParam {String[]} disabledScopes List of scopes that are disabled for this user ("imap", "pop3", "smtp")
@ -1319,6 +1335,8 @@ module.exports = (db, server, userHandler) => {
imapMaxUpload: Joi.number().min(0),
imapMaxDownload: Joi.number().min(0),
pop3MaxDownload: Joi.number().min(0),
imapMaxConnections: Joi.number().min(0),
receivedMax: Joi.number().min(0),
disable2fa: Joi.boolean()

View file

@ -1382,6 +1382,8 @@ class UserHandler {
imapMaxUpload: data.imapMaxUpload || 0,
imapMaxDownload: data.imapMaxDownload || 0,
pop3MaxDownload: data.pop3MaxDownload || 0,
imapMaxConnections: data.imapMaxConnections || 0,
receivedMax: data.receivedMax || 0,
targets: [].concat(data.targets || []),
@ -2831,6 +2833,7 @@ class UserHandler {
['receivedMax', 'rl:rcpt']
]);
let flushKeys = [];
let flushHKeys = [];
Object.keys(data).forEach(key => {
if (['user', 'existingPassword', 'hashedPassword', 'allowUnsafe', 'ip', 'sess'].includes(key)) {
@ -2840,6 +2843,9 @@ class UserHandler {
if (resetKeys.has(key)) {
flushKeys.push(resetKeys.get(key) + ':' + user);
}
if (key === 'imapMaxConnections') {
flushHKeys.push({ key: 'lim:imap', value: user.toString() });
}
if (key === 'password') {
if (!data[key]) {
@ -3042,11 +3048,17 @@ class UserHandler {
}
// check if we need to reset any ttl counters
if (flushKeys.length) {
if (flushKeys.length || flushHKeys.length) {
let flushreq = this.redis.multi();
flushKeys.forEach(key => {
flushreq = flushreq.del(key);
});
flushHKeys.forEach(entry => {
flushreq = flushreq.hdel(entry.key, entry.value);
});
// just call the operations and hope for the best, no problems if fails
flushreq.exec(() => false);
}
@ -3268,6 +3280,7 @@ class UserHandler {
setImmediate(tryDelete);
}
// returns a query to find an user based on address or username
checkAddress(username, callback) {
if (username.indexOf('@') < 0) {
// not formatted as an address, assume regular username
@ -3276,97 +3289,39 @@ class UserHandler {
});
}
let address = tools.normalizeAddress(username, false, {
removeLabel: true,
removeDots: true
});
let domain = address.substr(address.indexOf('@') + 1);
username = address.substr(0, address.indexOf('@'));
let aliasDomain;
let findAddressData = done => {
this.users.collection('addresses').findOne(
{
addrview: address
},
{
projection: {
user: true
},
maxTimeMS: consts.DB_MAX_TIME_USERS
},
(err, addressData) => {
if (err) {
err.code = 'InternalDatabaseError';
return callback(err);
}
if (addressData) {
return done(null, addressData);
}
// check if the address uses an alias domain
this.users.collection('domainaliases').findOne(
{ alias: domain },
{
maxTimeMS: consts.DB_MAX_TIME_USERS
},
(err, aliasData) => {
if (err) {
return done(err);
}
if (!aliasData) {
// not an alias domain, nothing to check for
return done();
}
aliasDomain = aliasData.domain;
this.users.collection('addresses').findOne(
{
addrview: username + '@' + aliasDomain
},
{
projection: {
user: true
},
maxTimeMS: consts.DB_MAX_TIME_USERS
},
(err, addressData) => {
if (err) {
err.code = 'InternalDatabaseError';
return done(err);
}
return done(null, addressData);
}
);
}
);
this.resolveAddress(
username,
{
wildcard: false,
projection: {
user: true
}
},
(err, addressData) => {
if (err) {
return callback(err);
}
);
};
findAddressData((err, addressData) => {
if (err) {
return callback(err);
}
if (addressData && !addressData.user) {
// found a non-user address
return callback(null, false);
}
if (addressData && !addressData.user) {
// found a non-user address
return callback(null, false);
}
if (!addressData) {
// fall back to username formatted as an address
return callback(null, {
unameview: tools.normalizeAddress(username, false, {
removeLabel: true,
removeDots: true
})
});
}
if (!addressData) {
// fall back to username formatted as an address
return callback(null, {
unameview: address
callback(null, {
_id: addressData.user
});
}
callback(null, {
_id: addressData.user
});
});
);
}
}

View file

@ -15,11 +15,11 @@
"author": "Andris Reinman",
"license": "EUPL-1.1+",
"devDependencies": {
"ajv": "6.8.1",
"ajv": "6.9.2",
"apidoc": "0.17.7",
"browserbox": "0.9.1",
"chai": "4.2.0",
"eslint": "5.13.0",
"eslint": "5.14.1",
"eslint-config-nodemailer": "1.2.0",
"eslint-config-prettier": "4.0.0",
"grunt": "1.0.3",
@ -30,7 +30,7 @@
"grunt-wait": "0.3.0",
"icedfrisby": "1.5.0",
"mailparser": "2.4.3",
"mocha": "5.2.0",
"mocha": "^5.2.0",
"request": "2.88.0"
},
"dependencies": {
@ -54,18 +54,18 @@
"libmime": "4.0.1",
"libqp": "1.1.0",
"mailsplit": "4.2.4",
"mobileconfig": "2.1.0",
"mobileconfig": "2.2.0",
"mongo-cursor-pagination": "7.1.0",
"mongodb": "3.1.13",
"mongodb-extended-json": "1.10.1",
"node-forge": "0.8.0",
"node-forge": "0.8.1",
"nodemailer": "5.1.1",
"npmlog": "4.1.2",
"openpgp": "4.4.6",
"pem": "1.14.1",
"openpgp": "4.4.7",
"pem": "1.14.2",
"pwnedpasswords": "1.0.4",
"qrcode": "1.3.3",
"restify": "7.7.0",
"restify": "8.0.0",
"restify-logger": "2.0.1",
"seq-index": "1.1.0",
"smtp-server": "3.5.0",
@ -74,7 +74,7 @@
"utf7": "1.0.2",
"uuid": "3.3.2",
"wild-config": "1.4.0",
"yargs": "12.0.5"
"yargs": "13.2.1"
},
"repository": {
"type": "git",

View file

@ -6,11 +6,11 @@ NODEREPO="node_10.x"
MONGODB="3.6"
CODENAME=`lsb_release -c -s`
WILDDUCK_COMMIT="dd5ea8b53e044fe535b4a72bbb6f46ae8acd51a4"
ZONEMTA_COMMIT="a7ed55745eac8c96c12e68f58b5f721bff094c5b" # zone-mta-template
WEBMAIL_COMMIT="6750c535a817af18b9c3046cd4e2ea143c661950"
WILDDUCK_ZONEMTA_COMMIT="cf2cd036061367b8fb096d34bc2da02b297098f0"
WILDDUCK_HARAKA_COMMIT="6e3bc18f253caf7b56a10a9ed033a0860b99f638"
WILDDUCK_COMMIT="2048e1e36575f2f38589be2c0a74968d41570e59"
ZONEMTA_COMMIT="2f43ae790600dd10c77eccb4149d95028401572c" # zone-mta-template
WEBMAIL_COMMIT="a8be1f60be76faf1bb5e49599b97b7f67b240710"
WILDDUCK_ZONEMTA_COMMIT="c5667b34a2bbb71811967135d1f7ac459d2063bb"
WILDDUCK_HARAKA_COMMIT="f3b5df4ed53763fb08be4fc6db99cbf1ece2ac93"
HARAKA_VERSION="2.8.23"
echo -e "\n-- Executing ${ORANGE}${OURNAME}${NC} subscript --"

View file

@ -11,3 +11,9 @@ apt-get -q -y install pwgen git ufw build-essential libssl-dev dnsutils python s
# rspamd
apt-get -q -y --no-install-recommends install rspamd
apt-get clean
# DMARC policy=reject rules
echo 'actions = {
quarantine = "add_header";
reject = "reject";
}' > /etc/rspamd/override.d/dmarc.conf

View file

@ -53,6 +53,7 @@ echo "$HOSTNAME" > config/me
echo "WildDuck MX" > config/smtpgreeting
echo "spf
dkim_verify
## ClamAV is disabled by default. Make sure freshclam has updated all
## virus definitions and clamav-daemon has successfully started before
@ -61,7 +62,6 @@ echo "spf
rspamd
tls
#dkim_verify
# WildDuck plugin handles recipient checking and queueing
wildduck" > config/plugins

View file

@ -38,7 +38,32 @@ echo "server {
ssl_certificate /etc/wildduck/certs/fullchain.pem;
ssl_certificate_key /etc/wildduck/certs/privkey.pem;
# special config for EventSource to disable gzip
location /api/events {
proxy_http_version 1.1;
gzip off;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header HOST \$http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://127.0.0.1:3000;
proxy_redirect off;
}
# special config for uploads
location /webmail/send {
client_max_body_size 15M;
proxy_http_version 1.1;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header HOST \$http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://127.0.0.1:3000;
proxy_redirect off;
}
location / {
proxy_http_version 1.1;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header HOST \$http_host;

View file

@ -1,6 +1,6 @@
# WildDuck Installer
Here you can find an example install script to install WildDuck with Haraka and ZoneMTA. The install script is self contained, you can upload to your server and start it as root. It fetches all required files from Github. After installation you should see exactly the same web interface as in https://wildduck.email/
Here you can find an example install script to install WildDuck with Haraka and ZoneMTA. The install script is self contained, you can upload to your server and start it as root. It fetches all required files from Github. After installation you should see exactly the same web interface as in https://webmail.wildduck.email/
The install script is tested on Ubuntu 16.04 and the server must be blank. Blank meaning that there should be no existing software installed (eg. Apache, MySQL or Postfix). If the server already has something installed, then remove the extra applications before running this script. This also means that you should not run the install script in a VPS that you already use for other stuff.