diff --git a/config/imap.toml b/config/imap.toml index 021f4b8..921f032 100644 --- a/config/imap.toml +++ b/config/imap.toml @@ -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 diff --git a/config/pop3.toml b/config/pop3.toml index 49c6125..1e66a8d 100644 --- a/config/pop3.toml +++ b/config/pop3.toml @@ -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 diff --git a/imap-core/lib/commands/append.js b/imap-core/lib/commands/append.js index ba0b571..f75ad9b 100644 --- a/imap-core/lib/commands/append.js +++ b/imap-core/lib/commands/append.js @@ -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 }); } diff --git a/imap-core/lib/commands/fetch.js b/imap-core/lib/commands/fetch.js index 36cb54c..fbc2197 100644 --- a/imap-core/lib/commands/fetch.js +++ b/imap-core/lib/commands/fetch.js @@ -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 }); } diff --git a/imap.js b/imap.js index 49d3073..ba34821 100644 --- a/imap.js +++ b/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); }; diff --git a/lib/api/addresses.js b/lib/api/addresses.js index 6c5064a..82702c1 100644 --- a/lib/api/addresses.js +++ b/lib/api/addresses.js @@ -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 || [] }; diff --git a/lib/api/submit.js b/lib/api/submit.js index d2b9fb2..f1c44fd 100644 --- a/lib/api/submit.js +++ b/lib/api/submit.js @@ -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 => { diff --git a/lib/api/users.js b/lib/api/users.js index 1c08964..defa11c 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -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 }, diff --git a/lib/consts.js b/lib/consts.js index 39a361d..0aa99db 100644 --- a/lib/consts.js +++ b/lib/consts.js @@ -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 }; diff --git a/lib/data-url.js b/lib/data-url.js index 2d4cf62..d05a96e 100644 --- a/lib/data-url.js +++ b/lib/data-url.js @@ -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; } diff --git a/lib/handlers/on-append.js b/lib/handlers/on-append.js index ef2a5a4..f93a24b 100644 --- a/lib/handlers/on-append.js +++ b/lib/handlers/on-append.js @@ -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); + } + ); + } + ); + }); }); }); }); diff --git a/lib/handlers/on-fetch.js b/lib/handlers/on-fetch.js index a558299..a0a494d 100644 --- a/lib/handlers/on-fetch.js +++ b/lib/handlers/on-fetch.js @@ -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); } diff --git a/lib/handlers/on-get-quota-root.js b/lib/handlers/on-get-quota-root.js index fbd8e86..e51a912 100644 --- a/lib/handlers/on-get-quota-root.js +++ b/lib/handlers/on-get-quota-root.js @@ -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) + }); }); } ); diff --git a/lib/handlers/on-get-quota.js b/lib/handlers/on-get-quota.js index f2428b8..ee2a467 100644 --- a/lib/handlers/on-get-quota.js +++ b/lib/handlers/on-get-quota.js @@ -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) + }); }); } ); diff --git a/lib/maildropper.js b/lib/maildropper.js index 2b3b105..738017c 100644 --- a/lib/maildropper.js +++ b/lib/maildropper.js @@ -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' }; } diff --git a/lib/pop3/connection.js b/lib/pop3/connection.js index 62dcd08..1ee4f97 100644 --- a/lib/pop3/connection.js +++ b/lib/pop3/connection.js @@ -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); } diff --git a/lib/settings-handler.js b/lib/settings-handler.js index 0811ba4..8ef0366 100644 --- a/lib/settings-handler.js +++ b/lib/settings-handler.js @@ -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 }); diff --git a/lib/tools.js b/lib/tools.js index 297d6b9..c5ed6f2 100644 --- a/lib/tools.js +++ b/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') { diff --git a/lib/user-cache.js b/lib/user-cache.js index bad157b..f08062e 100644 --- a/lib/user-cache.js +++ b/lib/user-cache.js @@ -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) diff --git a/lib/user-handler.js b/lib/user-handler.js index 8418bbf..98927ad 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -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; } diff --git a/pop3.js b/pop3.js index 40b71eb..e2f5e98 100644 --- a/pop3.js +++ b/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), diff --git a/test/api-test.js b/test/api-test.js index f3116c1..c6c638d 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -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('

Hello hello world! Red dot

'); + expect(messageData.html[0]).to.equal('

Hello hello world! Red dot

'); 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);