mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-09-20 07:16:05 +08:00
bunch of fixes
This commit is contained in:
parent
25e79a39f1
commit
04ba496dc7
|
@ -14,10 +14,12 @@ maxMB=25
|
|||
retention=30
|
||||
|
||||
# Default max donwload bandwith per day in megabytes
|
||||
maxDownloadMB=10000
|
||||
# Replaced by 'const:max:imap:download' setting
|
||||
maxDownloadMB=10240
|
||||
|
||||
# Default max upload bandwith per day in megabytes
|
||||
maxUploadMB=10000
|
||||
# Replaced by 'const:max:imap:upload' setting
|
||||
maxUploadMB=10240
|
||||
|
||||
# Default max concurrent connections per service per client
|
||||
maxConnections=15
|
||||
|
|
|
@ -15,7 +15,8 @@ disableVersionString=false
|
|||
maxMessages=250
|
||||
|
||||
# Max donwload bandwith per day in megabytes
|
||||
maxDownloadMB=10000
|
||||
# Replaced by 'const:max:pop3:download' setting
|
||||
maxDownloadMB=10240
|
||||
|
||||
# If true, then expect HAProxy PROXY header as the first line of data
|
||||
useProxy=false
|
||||
|
|
|
@ -153,6 +153,8 @@ module.exports = {
|
|||
logdata._error = err.message;
|
||||
logdata._code = err.code;
|
||||
logdata._response = err.response;
|
||||
logdata._responseMessage = err.responseMessage;
|
||||
logdata._ratelimit_ttl = err.ttl;
|
||||
this._server.loggelf(logdata);
|
||||
|
||||
if (err.code === 10334) {
|
||||
|
@ -166,7 +168,8 @@ module.exports = {
|
|||
// do not return actual error to user
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'TEMPFAIL'
|
||||
code: 'TEMPFAIL',
|
||||
message: err.responseMessage
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -311,10 +311,14 @@ module.exports = {
|
|||
logdata._error = err.message;
|
||||
logdata._code = err.code;
|
||||
logdata._response = err.response;
|
||||
logdata._responseMessage = err.responseMessage;
|
||||
logdata._ratelimit_ttl = err.ttl;
|
||||
this._server.loggelf(logdata);
|
||||
|
||||
return callback(null, {
|
||||
response: 'NO',
|
||||
code: 'TEMPFAIL'
|
||||
code: 'TEMPFAIL',
|
||||
message: err.responseMessage
|
||||
});
|
||||
}
|
||||
|
||||
|
|
82
imap.js
82
imap.js
|
@ -80,7 +80,7 @@ let createInterface = (ifaceOptions, callback) => {
|
|||
logger,
|
||||
|
||||
maxMessage: config.imap.maxMB * 1024 * 1024,
|
||||
maxStorage: ifaceOptions.maxStorage,
|
||||
settingsHandler: ifaceOptions.settingsHandler,
|
||||
|
||||
enableCompression: !!config.imap.enableCompression,
|
||||
|
||||
|
@ -249,50 +249,44 @@ module.exports = done => {
|
|||
|
||||
let settingsHandler = new SettingsHandler({ db: db.database });
|
||||
|
||||
settingsHandler
|
||||
.getMulti(['const:max:storage'])
|
||||
.then(settings => {
|
||||
let ifaceOptions = [
|
||||
{
|
||||
enabled: true,
|
||||
secure: config.imap.secure,
|
||||
disableSTARTTLS: config.imap.disableSTARTTLS || false,
|
||||
ignoreSTARTTLS: config.imap.ignoreSTARTTLS || false,
|
||||
host: config.imap.host,
|
||||
port: config.imap.port,
|
||||
let ifaceOptions = [
|
||||
{
|
||||
enabled: true,
|
||||
secure: config.imap.secure,
|
||||
disableSTARTTLS: config.imap.disableSTARTTLS || false,
|
||||
ignoreSTARTTLS: config.imap.ignoreSTARTTLS || false,
|
||||
host: config.imap.host,
|
||||
port: config.imap.port,
|
||||
settingsHandler
|
||||
}
|
||||
]
|
||||
.concat(config.imap.interface || [])
|
||||
.filter(iface => iface.enabled);
|
||||
|
||||
maxStorage: config.maxStorage ? config.maxStorage * 1024 * 1024 : settings['const:max:storage']
|
||||
}
|
||||
]
|
||||
.concat(config.imap.interface || [])
|
||||
.filter(iface => iface.enabled);
|
||||
let iPos = 0;
|
||||
let startInterfaces = () => {
|
||||
if (iPos >= ifaceOptions.length) {
|
||||
return db.redis.del('lim:imap', () => done());
|
||||
}
|
||||
let opts = ifaceOptions[iPos++];
|
||||
|
||||
let iPos = 0;
|
||||
let startInterfaces = () => {
|
||||
if (iPos >= ifaceOptions.length) {
|
||||
return db.redis.del('lim:imap', () => done());
|
||||
}
|
||||
let opts = ifaceOptions[iPos++];
|
||||
|
||||
createInterface(opts, err => {
|
||||
if (err) {
|
||||
logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'bind'
|
||||
},
|
||||
'Failed starting %sIMAP interface %s:%s. %s',
|
||||
opts.secure ? 'secure ' : '',
|
||||
opts.host,
|
||||
opts.port,
|
||||
err.message
|
||||
);
|
||||
return done(err);
|
||||
}
|
||||
setImmediate(startInterfaces);
|
||||
});
|
||||
};
|
||||
createInterface(opts, err => {
|
||||
if (err) {
|
||||
logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'bind'
|
||||
},
|
||||
'Failed starting %sIMAP interface %s:%s. %s',
|
||||
opts.secure ? 'secure ' : '',
|
||||
opts.host,
|
||||
opts.port,
|
||||
err.message
|
||||
);
|
||||
return done(err);
|
||||
}
|
||||
setImmediate(startInterfaces);
|
||||
})
|
||||
.catch(err => done(err));
|
||||
});
|
||||
};
|
||||
setImmediate(startInterfaces);
|
||||
};
|
||||
|
|
|
@ -202,7 +202,7 @@ module.exports = (db, server, userHandler, settingsHandler) => {
|
|||
user: addressData.user && addressData.user.toString(),
|
||||
forwarded: !!addressData.targets,
|
||||
forwardedDisabled: !!(addressData.targets && addressData.forwardedDisabled),
|
||||
targets: addressData.targets && addressData.targets.map(t => t.value),
|
||||
targets: addressData.targets && addressData.targets.map(target => target && target.value).filter(target => target),
|
||||
tags: addressData.tags || []
|
||||
};
|
||||
|
||||
|
|
|
@ -82,10 +82,7 @@ module.exports = (db, server, messageHandler, userHandler, settingsHandler) => {
|
|||
settingsHandler
|
||||
.getMulti(['const:max:storage', 'const:max:recipients', 'const:max:forwards'])
|
||||
.then(settings => {
|
||||
let overQuota =
|
||||
Number(userData.quota || (config.maxStorage ? config.maxStorage * 1024 * 1024 : settings['const:max:storage'])) -
|
||||
userData.storageUsed <=
|
||||
0;
|
||||
let overQuota = Number(userData.quota || settings['const:max:storage']) - userData.storageUsed <= 0;
|
||||
let maxRecipients = userData.recipients || config.maxRecipients || settings['const:max:recipients'];
|
||||
|
||||
let getReferencedMessage = done => {
|
||||
|
|
|
@ -219,13 +219,13 @@ module.exports = (db, server, userHandler, settingsHandler) => {
|
|||
name: userData.name,
|
||||
address: userData.address,
|
||||
tags: userData.tags || [],
|
||||
targets: userData.targets && userData.targets.map(t => t.value),
|
||||
targets: userData.targets && userData.targets.map(target => target.value).filter(target => target),
|
||||
enabled2fa: tools.getEnabled2fa(userData.enabled2fa),
|
||||
autoreply: !!userData.autoreply,
|
||||
encryptMessages: !!userData.encryptMessages,
|
||||
encryptForwarded: !!userData.encryptForwarded,
|
||||
quota: {
|
||||
allowed: Number(userData.quota) || (config.maxStorage ? config.maxStorage * 1024 * 1024 : settings['const:max:storage']),
|
||||
allowed: Number(userData.quota) || settings['const:max:storage'],
|
||||
used: Math.max(Number(userData.storageUsed) || 0, 0)
|
||||
},
|
||||
hasPasswordSet: !!userData.password || !!userData.tempPassword,
|
||||
|
@ -726,7 +726,14 @@ module.exports = (db, server, userHandler, settingsHandler) => {
|
|||
errors.notify(err, { userId: user });
|
||||
}
|
||||
|
||||
let settings = await settingsHandler.getMulti(['const:max:storage', 'const:max:recipients', 'const:max:forwards']);
|
||||
let settings = await settingsHandler.getMulti([
|
||||
'const:max:storage',
|
||||
'const:max:recipients',
|
||||
'const:max:forwards',
|
||||
'const:max:imap:upload',
|
||||
'const:max:imap:download',
|
||||
'const:max:pop3:download'
|
||||
]);
|
||||
|
||||
let recipients = Number(userData.recipients) || config.maxRecipients || settings['const:max:recipients'];
|
||||
let forwards = Number(userData.forwards) || config.maxForwards || settings['const:max:forwards'];
|
||||
|
@ -783,11 +790,14 @@ module.exports = (db, server, userHandler, settingsHandler) => {
|
|||
metaData: tools.formatMetaData(userData.metaData),
|
||||
internalData: tools.formatMetaData(userData.internalData),
|
||||
|
||||
targets: [].concat(userData.targets || []).map(targetData => targetData.value),
|
||||
targets: []
|
||||
.concat(userData.targets || [])
|
||||
.map(target => target.value)
|
||||
.filter(target => target),
|
||||
|
||||
limits: {
|
||||
quota: {
|
||||
allowed: Number(userData.quota) || (config.maxStorage ? config.maxStorage * 1024 * 1024 : settings['const:max:storage']),
|
||||
allowed: Number(userData.quota) || settings['const:max:storage'],
|
||||
used: Math.max(Number(userData.storageUsed) || 0, 0)
|
||||
},
|
||||
|
||||
|
@ -810,19 +820,19 @@ module.exports = (db, server, userHandler, settingsHandler) => {
|
|||
},
|
||||
|
||||
imapUpload: {
|
||||
allowed: Number(userData.imapMaxUpload) || (config.imap.maxUploadMB || 10000) * 1024 * 1024,
|
||||
allowed: Number(userData.imapMaxUpload) || settings['const:max:imap:upload'],
|
||||
used: imapUpload,
|
||||
ttl: imapUploadTtl >= 0 ? imapUploadTtl : false
|
||||
},
|
||||
|
||||
imapDownload: {
|
||||
allowed: Number(userData.imapMaxDownload) || (config.imap.maxDownloadMB || 10000) * 1024 * 1024,
|
||||
allowed: Number(userData.imapMaxDownload) || settings['const:max:imap:download'],
|
||||
used: imapDownload,
|
||||
ttl: imapDownloadTtl >= 0 ? imapDownloadTtl : false
|
||||
},
|
||||
|
||||
pop3Download: {
|
||||
allowed: Number(userData.pop3MaxDownload) || (config.pop3.maxDownloadMB || 10000) * 1024 * 1024,
|
||||
allowed: Number(userData.pop3MaxDownload) || settings['const:max:pop3:download'],
|
||||
used: pop3Download,
|
||||
ttl: pop3DownloadTtl >= 0 ? pop3DownloadTtl : false
|
||||
},
|
||||
|
|
|
@ -120,5 +120,14 @@ module.exports = {
|
|||
// Default maximum application password limit
|
||||
// Outlook limits to 40
|
||||
// https://support.microsoft.com/en-gb/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9
|
||||
MAX_ASP_COUNT: 50
|
||||
MAX_ASP_COUNT: 50,
|
||||
|
||||
// default max IMAP download size
|
||||
MAX_IMAP_DOWNLOAD: 10 * 1024 * 1024 * 1024,
|
||||
|
||||
// default max POP3 download size
|
||||
MAX_POP3_DOWNLOAD: 10 * 1024 * 1024 * 1024,
|
||||
|
||||
// default max IMAP upload size
|
||||
MAX_IMAP_UPLOAD: 10 * 1024 * 1024 * 1024
|
||||
};
|
||||
|
|
|
@ -47,7 +47,7 @@ function preprocessHtml(html, hostname) {
|
|||
let src = img.getAttribute('src');
|
||||
if (/^data:/.test(src)) {
|
||||
try {
|
||||
let attachment = processDataUrl(src, true);
|
||||
let attachment = processDataUrl({ href: src }, true);
|
||||
if (attachment) {
|
||||
let filename = img.getAttribute('data-filename');
|
||||
if (filename) {
|
||||
|
@ -74,6 +74,7 @@ function preprocessHtml(html, hostname) {
|
|||
|
||||
function preprocessAttachments(data) {
|
||||
let hostname = data.from && data.from.address && typeof data.from.address === 'string' ? data.from.address.split('@').pop() : os.hostname();
|
||||
|
||||
if (!data.html || !data.html.length || data.html.length > 12 * 1024 * 1024) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('wild-config');
|
||||
const db = require('../db');
|
||||
const consts = require('../consts');
|
||||
const tools = require('../tools');
|
||||
|
||||
// APPEND mailbox (flags) date message
|
||||
module.exports = (server, messageHandler, userCache) => (path, flags, date, raw, session, callback) => {
|
||||
|
@ -31,64 +31,74 @@ module.exports = (server, messageHandler, userCache) => (path, flags, date, raw,
|
|||
return callback(new Error('User not found'));
|
||||
}
|
||||
|
||||
if (userData.quota && userData.storageUsed > userData.quota) {
|
||||
return callback(false, 'OVERQUOTA');
|
||||
}
|
||||
|
||||
userCache.get(session.user.id, 'imapMaxUpload', (config.imap.maxUploadMB || 10) * 1024 * 1024, (err, limit) => {
|
||||
userCache.get(session.user.id, 'quota', { setting: 'const:max:storage' }, (err, quota) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
messageHandler.counters.ttlcounter('iup:' + session.user.id, 0, limit, false, (err, res) => {
|
||||
|
||||
if (quota && userData.storageUsed > quota) {
|
||||
return callback(false, 'OVERQUOTA');
|
||||
}
|
||||
|
||||
userCache.get(session.user.id, 'imapMaxUpload', { setting: 'const:max:imap:upload' }, (err, limit) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!res.success) {
|
||||
let err = new Error('Upload was rate limited. Try again in ' + res.ttl + ' seconds');
|
||||
err.response = 'NO';
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
messageHandler.counters.ttlcounter('iup:' + session.user.id, raw.length, limit, false, () => {
|
||||
flags = Array.isArray(flags) ? flags : [].concat(flags || []);
|
||||
messageHandler.counters.ttlcounter('iup:' + session.user.id, 0, limit, false, (err, res) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!res.success) {
|
||||
let err = new Error('Upload was rate limited');
|
||||
err.response = 'NO';
|
||||
err.code = 'UploadRateLimited';
|
||||
err.ttl = res.ttl;
|
||||
err.responseMessage = `Upload was rate limited. Try again in ${tools.roundTime(res.ttl)}.`;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
messageHandler.encryptMessage(
|
||||
userData.encryptMessages && !flags.includes('\\Draft') ? userData.pubKey : false,
|
||||
raw,
|
||||
(err, encrypted) => {
|
||||
if (!err && encrypted) {
|
||||
raw = encrypted;
|
||||
}
|
||||
messageHandler.add(
|
||||
{
|
||||
user: session.user.id,
|
||||
path,
|
||||
meta: {
|
||||
source: 'IMAP',
|
||||
from: '',
|
||||
to: [session.user.address || session.user.username],
|
||||
origin: session.remoteAddress,
|
||||
transtype: 'APPEND',
|
||||
time: new Date()
|
||||
},
|
||||
session,
|
||||
date,
|
||||
flags,
|
||||
raw
|
||||
},
|
||||
(err, status, data) => {
|
||||
if (err) {
|
||||
if (err.imapResponse) {
|
||||
return callback(null, err.imapResponse);
|
||||
}
|
||||
messageHandler.counters.ttlcounter('iup:' + session.user.id, raw.length, limit, false, () => {
|
||||
flags = Array.isArray(flags) ? flags : [].concat(flags || []);
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, status, data);
|
||||
messageHandler.encryptMessage(
|
||||
userData.encryptMessages && !flags.includes('\\Draft') ? userData.pubKey : false,
|
||||
raw,
|
||||
(err, encrypted) => {
|
||||
if (!err && encrypted) {
|
||||
raw = encrypted;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
messageHandler.add(
|
||||
{
|
||||
user: session.user.id,
|
||||
path,
|
||||
meta: {
|
||||
source: 'IMAP',
|
||||
from: '',
|
||||
to: [session.user.address || session.user.username],
|
||||
origin: session.remoteAddress,
|
||||
transtype: 'APPEND',
|
||||
time: new Date()
|
||||
},
|
||||
session,
|
||||
date,
|
||||
flags,
|
||||
raw
|
||||
},
|
||||
(err, status, data) => {
|
||||
if (err) {
|
||||
if (err.imapResponse) {
|
||||
return callback(null, err.imapResponse);
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, status, data);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('wild-config');
|
||||
const IMAPServerModule = require('../../imap-core');
|
||||
const imapHandler = IMAPServerModule.imapHandler;
|
||||
const db = require('../db');
|
||||
|
@ -41,7 +40,7 @@ module.exports = (server, messageHandler, userCache) => (mailbox, options, sessi
|
|||
return callback(null, 'NONEXISTENT');
|
||||
}
|
||||
|
||||
userCache.get(session.user.id, 'imapMaxDownload', (config.imap.maxDownloadMB || 10000) * 1024 * 1024, (err, limit) => {
|
||||
userCache.get(session.user.id, 'imapMaxDownload', { setting: 'const:max:imap:download' }, (err, limit) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
@ -51,9 +50,11 @@ module.exports = (server, messageHandler, userCache) => (mailbox, options, sessi
|
|||
return callback(err);
|
||||
}
|
||||
if (!res.success) {
|
||||
let err = new Error('Download was rate limited. Check again in ' + res.ttl + ' seconds');
|
||||
let err = new Error('Download was rate limited');
|
||||
err.response = 'NO';
|
||||
err.code = 'DownloadRateLimited';
|
||||
err.ttl = res.ttl;
|
||||
err.responseMessage = `Download was rate limited. Try again in ${tools.roundTime(res.ttl)}.`;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
|
|
|
@ -45,10 +45,31 @@ module.exports = server => (path, session, callback) => {
|
|||
return callback(new Error('User data not found'));
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
root: '',
|
||||
quota: user.quota || server.options.maxStorage || 0,
|
||||
storageUsed: Math.max(user.storageUsed || 0, 0)
|
||||
let getQuota = next => {
|
||||
if (user.quota) {
|
||||
return next(null, user.quota);
|
||||
}
|
||||
|
||||
if (!server.options.settingsHandler) {
|
||||
return next(null, 0);
|
||||
}
|
||||
|
||||
server.options.settingsHandler
|
||||
.get('const:max:storage')
|
||||
.then(maxStorage => next(null, maxStorage))
|
||||
.catch(err => next(err));
|
||||
};
|
||||
|
||||
getQuota((err, maxStorage) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
root: '',
|
||||
quota: user.quota || maxStorage || 0,
|
||||
storageUsed: Math.max(user.storageUsed || 0, 0)
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -33,10 +33,31 @@ module.exports = server => (quotaRoot, session, callback) => {
|
|||
return callback(new Error('User data not found'));
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
root: '',
|
||||
quota: user.quota || server.options.maxStorage || 0,
|
||||
storageUsed: Math.max(user.storageUsed || 0, 0)
|
||||
let getQuota = next => {
|
||||
if (user.quota) {
|
||||
return next(null, user.quota);
|
||||
}
|
||||
|
||||
if (!server.options.settingsHandler) {
|
||||
return next(null, 0);
|
||||
}
|
||||
|
||||
server.options.settingsHandler
|
||||
.get('const:max:storage')
|
||||
.then(maxStorage => next(null, maxStorage))
|
||||
.catch(err => next(err));
|
||||
};
|
||||
|
||||
getQuota((err, maxStorage) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
root: '',
|
||||
quota: user.quota || maxStorage || 0,
|
||||
storageUsed: Math.max(user.storageUsed || 0, 0)
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -549,7 +549,7 @@ class Maildropper {
|
|||
return { success: false, code: 'NoSuchQueueEntry' };
|
||||
}
|
||||
|
||||
if (user && !user.equals(queueFile.metadata.data.user)) {
|
||||
if (user && queueFile.metadata.data.userId && user.toString() !== queueFile.metadata.data.userId.toString()) {
|
||||
// message does not belong to us
|
||||
return { success: false, code: 'NotEnoughPrivileges' };
|
||||
}
|
||||
|
|
|
@ -286,7 +286,7 @@ class POP3Connection extends EventEmitter {
|
|||
command,
|
||||
err.message
|
||||
);
|
||||
this.send('-ERR ' + err.message);
|
||||
this.send('-ERR ' + (err.responseMessage || err.message));
|
||||
this.close();
|
||||
} else {
|
||||
this.processQueue();
|
||||
|
@ -693,6 +693,25 @@ class POP3Connection extends EventEmitter {
|
|||
|
||||
this._server.onFetchMessage(message, this.session, (err, stream) => {
|
||||
if (err) {
|
||||
this._server.loggelf({
|
||||
short_message: '[POP3RETR] error',
|
||||
_mail_action: 'pop3_retr',
|
||||
_message_id: message.id,
|
||||
_message_uid: message.uid,
|
||||
_mailbox: message.mailbox,
|
||||
_message_size: message.size,
|
||||
_username: this.session.user && this.session.user.username,
|
||||
_user: this.session.user && this.session.user.id,
|
||||
_sess: this.id,
|
||||
_ip: this.remoteAddress,
|
||||
|
||||
_error: err.message,
|
||||
_code: err.code,
|
||||
_response: err.response,
|
||||
_responseMessage: err.responseMessage,
|
||||
_ratelimit_ttl: err.ttl
|
||||
});
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
|
@ -706,7 +725,7 @@ class POP3Connection extends EventEmitter {
|
|||
message.fetched = true;
|
||||
|
||||
this._server.loggelf({
|
||||
short_message: '[POP3RETR]',
|
||||
short_message: '[POP3RETR] OK',
|
||||
_mail_action: 'pop3_retr',
|
||||
_message_id: message.id,
|
||||
_message_uid: message.uid,
|
||||
|
@ -757,6 +776,25 @@ class POP3Connection extends EventEmitter {
|
|||
|
||||
this._server.onFetchMessage(message, this.session, (err, stream) => {
|
||||
if (err) {
|
||||
this._server.loggelf({
|
||||
short_message: '[POP3TOP] error',
|
||||
_mail_action: 'pop3_top',
|
||||
_message_id: message.id,
|
||||
_message_uid: message.uid,
|
||||
_mailbox: message.mailbox,
|
||||
_message_size: message.size,
|
||||
_username: this.session.user && this.session.user.username,
|
||||
_user: this.session.user && this.session.user.id,
|
||||
_sess: this.id,
|
||||
_ip: this.remoteAddress,
|
||||
|
||||
_error: err.message,
|
||||
_code: err.code,
|
||||
_response: err.response,
|
||||
_responseMessage: err.responseMessage,
|
||||
_ratelimit_ttl: err.ttl
|
||||
});
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ const { encrypt, decrypt } = require('./encrypt');
|
|||
const consts = require('./consts');
|
||||
const Joi = require('joi');
|
||||
const tools = require('./tools');
|
||||
const config = require('wild-config');
|
||||
|
||||
const SETTING_KEYS = [
|
||||
{
|
||||
|
@ -20,7 +21,8 @@ const SETTING_KEYS = [
|
|||
name: 'Disk quota',
|
||||
description: 'Maximum allowed storage size in bytes',
|
||||
type: 'size',
|
||||
constKey: 'MAX_STORAGE',
|
||||
constKey: false,
|
||||
confValue: (Number(config.maxStorage) || 0) * 1024 * 1024 || consts.MAX_STORAGE,
|
||||
schema: Joi.number()
|
||||
},
|
||||
|
||||
|
@ -67,6 +69,35 @@ const SETTING_KEYS = [
|
|||
type: 'number',
|
||||
constKey: 'MAX_ASP_COUNT',
|
||||
schema: Joi.number()
|
||||
},
|
||||
|
||||
{
|
||||
key: 'const:max:imap:download',
|
||||
name: 'Max IMAP download',
|
||||
description: 'Maximum default daily IMAP download size',
|
||||
type: 'size',
|
||||
constKey: false,
|
||||
confValue: ((config.imap && config.imap.maxDownloadMB) || consts.MAX_IMAP_DOWNLOAD) * 1024 * 1024,
|
||||
schema: Joi.number()
|
||||
},
|
||||
{
|
||||
key: 'const:max:pop3:download',
|
||||
name: 'Max POP3 download',
|
||||
description: 'Maximum default daily POP3 download size',
|
||||
type: 'size',
|
||||
constKey: false,
|
||||
confValue: ((config.pop3 && config.pop3.maxDownloadMB) || consts.MAX_POP3_DOWNLOAD) * 1024 * 1024,
|
||||
schema: Joi.number()
|
||||
},
|
||||
|
||||
{
|
||||
key: 'const:max:imap:upload',
|
||||
name: 'Max IMAP upload',
|
||||
description: 'Maximum default daily IMAP upload size',
|
||||
type: 'size',
|
||||
constKey: false,
|
||||
confValue: ((config.imap && config.imap.maxUploadMB) || consts.MAX_IMAP_UPLOAD) * 1024 * 1024,
|
||||
schema: Joi.number()
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -147,7 +178,9 @@ class SettingsHandler {
|
|||
result[key] = JSON.parse(row.value);
|
||||
} else {
|
||||
let keyInfo = this.keys.find(k => k.key === key) || {};
|
||||
let defaultValue = 'default' in options ? options.default : keyInfo.constKey ? consts[keyInfo.constKey] : undefined;
|
||||
|
||||
let confDefaultValue = keyInfo.constKey ? consts[keyInfo.constKey] : keyInfo.confValue;
|
||||
let defaultValue = 'default' in options ? options.default : confDefaultValue;
|
||||
|
||||
result[key] = row ? row.value : defaultValue;
|
||||
}
|
||||
|
@ -174,7 +207,9 @@ class SettingsHandler {
|
|||
}
|
||||
|
||||
let keyInfo = this.keys.find(k => k.key === key) || {};
|
||||
let defaultValue = 'default' in options ? options.default : keyInfo.constKey ? consts[keyInfo.constKey] : undefined;
|
||||
|
||||
let confDefaultValue = keyInfo.constKey ? consts[keyInfo.constKey] : keyInfo.confValue;
|
||||
let defaultValue = 'default' in options ? options.default : confDefaultValue;
|
||||
|
||||
return row ? row.value : defaultValue;
|
||||
}
|
||||
|
@ -216,7 +251,7 @@ class SettingsHandler {
|
|||
value: row.value,
|
||||
name: keyInfo.name,
|
||||
description: keyInfo.description,
|
||||
default: keyInfo.constKey ? consts[keyInfo.constKey] : undefined,
|
||||
default: keyInfo.constKey ? consts[keyInfo.constKey] : keyInfo.confValue,
|
||||
type: keyInfo.type,
|
||||
custom: true
|
||||
});
|
||||
|
@ -232,10 +267,10 @@ class SettingsHandler {
|
|||
|
||||
results.push({
|
||||
key: row.key,
|
||||
value: row.constKey ? consts[row.constKey] : undefined,
|
||||
value: row.constKey ? consts[row.constKey] : row.confValue,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
default: row.constKey ? consts[row.constKey] : undefined,
|
||||
default: row.constKey ? consts[row.constKey] : row.confValue,
|
||||
type: row.type,
|
||||
custom: false
|
||||
});
|
||||
|
|
20
lib/tools.js
20
lib/tools.js
|
@ -567,6 +567,25 @@ function getEnabled2fa(enabled2fa) {
|
|||
return list;
|
||||
}
|
||||
|
||||
function roundTime(seconds) {
|
||||
let days = Math.floor(seconds / (24 * 3600));
|
||||
if (days) {
|
||||
return `${days} ${days === 1 ? 'day' : 'days'}`;
|
||||
}
|
||||
|
||||
let hours = Math.floor(seconds / 3600);
|
||||
if (hours) {
|
||||
return `${hours} ${hours === 1 ? 'hour' : 'hours'}`;
|
||||
}
|
||||
|
||||
let minutes = Math.floor(seconds / 3600);
|
||||
if (minutes) {
|
||||
return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`;
|
||||
}
|
||||
|
||||
return `${seconds} ${seconds === 1 ? 'second' : 'seconds'}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeAddress,
|
||||
normalizeDomain,
|
||||
|
@ -589,6 +608,7 @@ module.exports = {
|
|||
getPGPUserId,
|
||||
formatFingerprint,
|
||||
getEnabled2fa,
|
||||
roundTime,
|
||||
|
||||
formatMetaData: metaData => {
|
||||
if (typeof metaData === 'string') {
|
||||
|
|
|
@ -4,12 +4,27 @@ class UserCache {
|
|||
constructor(options) {
|
||||
this.users = options.users;
|
||||
this.redis = options.redis;
|
||||
this.settingsHandler = options.settingsHandler;
|
||||
}
|
||||
|
||||
flush(user, callback) {
|
||||
this.redis.del('cached:' + user, () => callback());
|
||||
}
|
||||
|
||||
getDefaultValue(defaultValue, callback) {
|
||||
if (defaultValue && typeof defaultValue === 'object' && defaultValue.setting && typeof defaultValue.setting === 'string') {
|
||||
this.settingsHandler
|
||||
.get(defaultValue.setting)
|
||||
.then(value => {
|
||||
callback(null, value);
|
||||
})
|
||||
.catch(err => callback(err));
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, defaultValue);
|
||||
}
|
||||
|
||||
get(user, key, defaultValue, callback) {
|
||||
this.redis.hget('cached:' + user, key, (err, value) => {
|
||||
if (err) {
|
||||
|
@ -34,11 +49,11 @@ class UserCache {
|
|||
return callback(err);
|
||||
}
|
||||
|
||||
if (!userData) {
|
||||
return callback(null, defaultValue);
|
||||
if (!userData || !userData[key]) {
|
||||
return this.getDefaultValue(defaultValue, callback);
|
||||
}
|
||||
|
||||
value = userData[key] || defaultValue;
|
||||
value = userData[key];
|
||||
this.redis
|
||||
.multi()
|
||||
.hset('cached:' + user, key, value)
|
||||
|
|
|
@ -53,15 +53,17 @@ class UserHandler {
|
|||
this.messageHandler = options.messageHandler;
|
||||
this.counters = this.messageHandler ? this.messageHandler.counters : counters(this.redis);
|
||||
|
||||
this.settingsHandler = new SettingsHandler({ db: this.database });
|
||||
|
||||
this.userCache = new UserCache({
|
||||
users: this.users,
|
||||
redis: this.redis
|
||||
redis: this.redis,
|
||||
settingsHandler: this.settingsHandler
|
||||
});
|
||||
|
||||
this.flushUserCache = util.promisify(this.userCache.flush.bind(this.userCache));
|
||||
|
||||
this.taskHandler = new TaskHandler({ database: this.database });
|
||||
this.settingsHandler = new SettingsHandler({ db: this.database });
|
||||
}
|
||||
|
||||
resolveAddress(address, options, callback) {
|
||||
|
@ -1415,7 +1417,7 @@ class UserHandler {
|
|||
|
||||
receivedMax: data.receivedMax || 0,
|
||||
|
||||
targets: [].concat(data.targets || []),
|
||||
targets: [].concat(data.targets || []).filter(target => target),
|
||||
|
||||
// autoreply status
|
||||
// off by default, can be changed later by user through the API
|
||||
|
@ -2820,7 +2822,7 @@ class UserHandler {
|
|||
},
|
||||
$pull: {
|
||||
$pull: {
|
||||
enabled2fa: 'totp'
|
||||
enabled2fa: 'webauthn'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -3732,10 +3734,12 @@ class UserHandler {
|
|||
}
|
||||
|
||||
function rateLimitResponse(res) {
|
||||
let err = new Error('Authentication was rate limited. Check again in ' + res.ttl + ' seconds');
|
||||
let err = new Error('Authentication was rate limited');
|
||||
err.response = 'NO';
|
||||
err.responseCode = 403;
|
||||
err.ttl = res.ttl;
|
||||
err.code = 'RateLimitedError';
|
||||
err.responseMessage = `Authentication was rate limited. Try again in ${tools.roundTime(res.ttl)}.`;
|
||||
return err;
|
||||
}
|
||||
|
||||
|
|
10
pop3.js
10
pop3.js
|
@ -10,6 +10,7 @@ const ObjectId = require('mongodb').ObjectId;
|
|||
const db = require('./lib/db');
|
||||
const certs = require('./lib/certs');
|
||||
const LimitedFetch = require('./lib/limited-fetch');
|
||||
const tools = require('./lib/tools');
|
||||
const Gelf = require('gelf');
|
||||
const os = require('os');
|
||||
|
||||
|
@ -204,7 +205,7 @@ const serverOptions = {
|
|||
},
|
||||
|
||||
onFetchMessage(message, session, callback) {
|
||||
userHandler.userCache.get(session.user.id, 'pop3MaxDownload', (config.pop3.maxDownloadMB || 10000) * 1024 * 1024, (err, limit) => {
|
||||
userHandler.userCache.get(session.user.id, 'pop3MaxDownload', { setting: 'const:max:pop3:download' }, (err, limit) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
@ -214,9 +215,14 @@ const serverOptions = {
|
|||
return callback(err);
|
||||
}
|
||||
if (!res.success) {
|
||||
let err = new Error('Download was rate limited. Check again in ' + res.ttl + ' seconds');
|
||||
let err = new Error('Download was rate limited');
|
||||
err.response = 'NO';
|
||||
err.code = 'DownloadRateLimited';
|
||||
err.ttl = res.ttl;
|
||||
err.responseMessage = `Download was rate limited. Try again in ${tools.roundTime(res.ttl)}.`;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
db.database.collection('messages').findOne(
|
||||
{
|
||||
_id: new ObjectId(message.id),
|
||||
|
|
|
@ -553,8 +553,9 @@ describe('API tests', function () {
|
|||
expect(response.body.success).to.be.true;
|
||||
|
||||
const messageData = messageDataResponse.body;
|
||||
|
||||
expect(messageData.subject).to.equal(message.subject);
|
||||
expect(messageData.html[0]).to.equal('<p>Hello hello world! <img src="attachment:ATT00001" alt="Red dot" /></p>');
|
||||
expect(messageData.html[0]).to.equal('<p>Hello hello world! <img src="attachment:ATT00001" alt="Red dot"></p>');
|
||||
expect(messageData.attachments).to.deep.equal([
|
||||
{
|
||||
contentType: 'image/png',
|
||||
|
@ -600,6 +601,7 @@ describe('API tests', function () {
|
|||
const sentMessageDataResponse = await server.get(
|
||||
`/users/${userId}/mailboxes/${submitResponse.body.message.mailbox}/messages/${submitResponse.body.message.id}`
|
||||
);
|
||||
|
||||
expect(sentMessageDataResponse.body.outbound[0].queueId).to.equal(submitResponse.body.queueId);
|
||||
|
||||
const deleteResponse = await server.delete(`/users/${userId}/outbound/${submitResponse.body.queueId}`).expect(200);
|
||||
|
|
Loading…
Reference in a new issue