bunch of fixes

This commit is contained in:
Andris Reinman 2022-07-05 11:57:57 +03:00
parent 25e79a39f1
commit 04ba496dc7
No known key found for this signature in database
GPG key ID: DC6C83F4D584D364
22 changed files with 338 additions and 144 deletions

View file

@ -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

View file

@ -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

View file

@ -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
});
}

View file

@ -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
View file

@ -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);
};

View file

@ -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 || []
};

View file

@ -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 => {

View file

@ -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
},

View file

@ -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
};

View file

@ -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;
}

View file

@ -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);
}
);
}
);
});
});
});
});

View file

@ -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);
}

View file

@ -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)
});
});
}
);

View file

@ -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)
});
});
}
);

View file

@ -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' };
}

View file

@ -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);
}

View file

@ -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
});

View file

@ -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') {

View file

@ -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)

View file

@ -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
View file

@ -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),

View file

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