diff --git a/lib/api/mailboxes.js b/lib/api/mailboxes.js index 6fc45810..e1f3d105 100644 --- a/lib/api/mailboxes.js +++ b/lib/api/mailboxes.js @@ -14,15 +14,7 @@ module.exports = (db, server, mailboxHandler) => { const getMailboxCounter = util.promisify(tools.getMailboxCounter); const updateMailbox = util.promisify(mailboxHandler.update.bind(mailboxHandler)); const deleteMailbox = util.promisify(mailboxHandler.del.bind(mailboxHandler)); - const createMailbox = util.promisify((...args) => { - let callback = args.pop(); - mailboxHandler.create(...args, (err, status, id) => { - if (err) { - return callback(err); - } - return callback(null, { status, id }); - }); - }); + const createMailbox = mailboxHandler.createAsync.bind(mailboxHandler); server.get( '/users/:user/mailboxes', diff --git a/lib/api/messages.js b/lib/api/messages.js index 08e6cea5..c56e2745 100644 --- a/lib/api/messages.js +++ b/lib/api/messages.js @@ -2248,7 +2248,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti envelope, files }, - session: result.value.session, + session: result.value.sess, date, verificationResults, flags: [] @@ -2725,7 +2725,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti }); } - let maxRecipients = Number(userData.maxRecipients) || (await settingsHandler.get('const:max:recipients')); + let maxRecipients = Number(userData.recipients) || (await settingsHandler.get('const:max:recipients')); let maxRptsTo = await settingsHandler.get('const:max:rcpt_to'); // Trying to send more than allowed recipients count per email @@ -2873,6 +2873,26 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti } } + for (const to of Array.isArray(envelope.to) ? envelope.to : [envelope.to]) { + server.loggelf({ + short_message: `[RCPT TO: ${to}] ${result.value.sess}`, + _mail_action: 'rcpt_to', + _user: userData._id.toString(), + _queue_id: queueId, + _sent_mailbox: response.message && response.message.mailbox, + _sent_message: response.message && response.message.id, + _send_time: sendTime && sendTime.toISOString && sendTime.toISOString(), + _from: envelope.from, + _to: to, + _message_id: messageData.msgid, + _subject: messageData.subject, + _sess: result.value.sess, + _ip: result.value.ip, + _limit_allowed: userData.recipients, + _limit_sent: messagesSent + envelope.to.length + }); + } + server.loggelf({ short_message: '[SUBMIT] draft', _mail_action: 'submit_draft', @@ -2885,7 +2905,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti _to: envelope.to && envelope.to.join(','), _message_id: messageData.msgid, _subject: messageData.subject, - _sess: result.value.session, + _sess: result.value.sess, _ip: result.value.ip }); diff --git a/lib/audit-handler.js b/lib/audit-handler.js index f9df3a37..c11319d9 100644 --- a/lib/audit-handler.js +++ b/lib/audit-handler.js @@ -262,7 +262,8 @@ class AuditHandler { let expiredAudits = await this.database .collection('audits') .find({ - expires: { $lt: new Date(), deleted: false } + expires: { $lt: new Date() }, + deleted: false }) .toArray(); diff --git a/lib/consts.js b/lib/consts.js index d6c971b2..7eda33a5 100644 --- a/lib/consts.js +++ b/lib/consts.js @@ -134,5 +134,8 @@ module.exports = { MAX_IMAP_UPLOAD: 10 * 1024 * 1024 * 1024, // maximum number of filters per account - MAX_FILTERS: 400 + MAX_FILTERS: 400, + + // maximum amount of mailboxes per user + MAX_MAILBOXES: 1500 }; diff --git a/lib/mailbox-handler.js b/lib/mailbox-handler.js index da3b5b68..aa8be17d 100644 --- a/lib/mailbox-handler.js +++ b/lib/mailbox-handler.js @@ -3,6 +3,7 @@ const ObjectId = require('mongodb').ObjectId; const ImapNotifier = require('./imap-notifier'); const { publish, MAILBOX_CREATED, MAILBOX_RENAMED, MAILBOX_DELETED } = require('./events'); +const { SettingsHandler } = require('./settings-handler'); class MailboxHandler { constructor(options) { @@ -19,99 +20,92 @@ class MailboxHandler { redis: this.redis, pushOnly: true }); + + this.settingsHandler = new SettingsHandler({ db: this.database }); } create(user, path, opts, callback) { - this.database.collection('mailboxes').findOne( - { - user, - path - }, - (err, mailboxData) => { - if (err) { - return callback(err); - } - if (mailboxData) { - const err = new Error('Mailbox creation failed with code MailboxAlreadyExists'); - err.code = 'ALREADYEXISTS'; - err.responseCode = 400; - return callback(err, 'ALREADYEXISTS'); - } + this.createAsync(user, path, opts) + .then(mailboxData => callback(null, ...[mailboxData.status, mailboxData.id])) + .catch(err => callback(err)); + } - this.users.collection('users').findOne( - { - _id: user - }, - { - projection: { - retention: true - } - }, - (err, userData) => { - if (err) { - return callback(err); - } + async createAsync(user, path, opts) { + const userData = await this.users.collection('users').findOne({ _id: user }, { projection: { retention: true } }); - if (!userData) { - const err = new Error('This user does not exist'); - err.code = 'UserNotFound'; - err.responseCode = 404; - return callback(err, 'UserNotFound'); - } + if (!userData) { + const err = new Error('This user does not exist'); + err.code = 'UserNotFound'; + err.responseCode = 404; + throw err; + } - mailboxData = { - _id: new ObjectId(), - user, - path, - uidValidity: Math.floor(Date.now() / 1000), - uidNext: 1, - modifyIndex: 0, - subscribed: true, - flags: [], - retention: userData.retention - }; + let mailboxData = await this.database.collection('mailboxes').findOne({ user, path }); - Object.keys(opts || {}).forEach(key => { - if (!['_id', 'user', 'path'].includes(key)) { - mailboxData[key] = opts[key]; - } - }); + if (mailboxData) { + const err = new Error('Mailbox creation failed with code MailboxAlreadyExists'); + err.code = 'ALREADYEXISTS'; + err.responseCode = 400; + throw err; + } - this.database.collection('mailboxes').insertOne(mailboxData, { writeConcern: 'majority' }, (err, r) => { - if (err) { - if (err.code === 11000) { - const err = new Error('Mailbox creation failed with code MailboxAlreadyExists'); - err.code = 'ALREADYEXISTS'; - err.responseCode = 400; - return callback(err, 'ALREADYEXISTS'); - } - return callback(err); - } + const mailboxCountForUser = await this.database.collection('mailboxes').countDocuments({ user }); - publish(this.redis, { - ev: MAILBOX_CREATED, - user, - mailbox: r.insertedId, - path: mailboxData.path - }).catch(() => false); + if (mailboxCountForUser > (await this.settingsHandler.get('const:max:mailboxes'))) { + const err = new Error('Mailbox creation failed with code ReachedMailboxCountLimit. Max mailboxes count reached.'); + err.code = 'CANNOT'; + err.responseCode = 400; + throw err; + } - return this.notifier.addEntries( - mailboxData, - { - command: 'CREATE', - mailbox: r.insertedId, - path - }, - () => { - this.notifier.fire(user); - return callback(null, true, mailboxData._id); - } - ); - }); - } - ); + mailboxData = { + _id: new ObjectId(), + user, + path, + uidValidity: Math.floor(Date.now() / 1000), + uidNext: 1, + modifyIndex: 0, + subscribed: true, + flags: [], + retention: userData.retention + }; + + Object.keys(opts || {}).forEach(key => { + if (!['_id', 'user', 'path'].includes(key)) { + mailboxData[key] = opts[key]; + } + }); + + const r = this.database.collection('mailboxes').insertOne(mailboxData, { writeConcern: 'majority' }); + + try { + await publish(this.redis, { + ev: MAILBOX_CREATED, + user, + mailbox: r.insertedId, + path: mailboxData.path + }); + } catch { + // ignore + } + + await this.notifier.addEntries( + mailboxData, + { + command: 'CREATE', + mailbox: r.insertedId, + path + }, + () => { + this.notifier.fire(user); + return; } ); + + return { + status: true, + id: mailboxData._id + }; } rename(user, mailbox, newname, opts, callback) { diff --git a/lib/settings-handler.js b/lib/settings-handler.js index e68cc043..84018e61 100644 --- a/lib/settings-handler.js +++ b/lib/settings-handler.js @@ -35,6 +35,15 @@ const SETTING_KEYS = [ schema: Joi.number() }, + { + key: 'const:max:mailboxes', + name: 'Max mailboxes', + description: 'Maximum amount of mailboxes for a user', + type: 'number', + constKey: 'MAX_MAILBOXES', + schema: Joi.number() + }, + { key: 'const:max:rcpt_to', name: 'Max message recipients', diff --git a/lib/tools.js b/lib/tools.js index 0e9e59d1..2970082c 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -870,44 +870,63 @@ module.exports = { }, async generateAPiDocs(routes) { - let docs = ` -openapi: 3.0.0 -info: - title: WildDuck API - description: WildDuck API docs - version: 1.0.0 - contact: - url: 'https://github.com/nodemailer/wildduck' - -servers: - - url: 'https://api.wildduck.email' - -tags: - - name: Addresses - - name: ApplicationPasswords - - name: Archive - description: Archive includes all deleted messages. Once messages are old enough then these are permanenetly deleted from the archive as well. Until then you can restore the deleted messages. - - name: Audit - description: 'Auditing allows to monitor an email account. All existing, deleted and new emails are copied to the auditing system. See also https://github.com/nodemailer/wildduck-audit-manager' - - name: Authentication - - name: Autoreplies - - name: Certs - description: WildDuck allows to register TLS certificates to be used with SNI connections. These certificates are used by IMAP, POP3, API and SMTP servers when a SNI capable client establishes a TLS connection. This does not apply for MX servers. - - name: DKIM - description: Whenever an email is sent WildDuck checks if there is a DKIM key registered for the domain name of the sender address and uses it to sign the message. - - name: DomainAccess - description: Add sender domain names to allowlist (messages are all accepted) or blocklist (messages are sent to Spam folder) - - name: DomainAliases - - name: Filters - - name: Mailboxes - - name: Messages - - name: Settings - - name: Storage - description: Storage allows easier attachment handling when composing Draft messages. Instead of uploading the attachmnent with every draft update, you store the attachment to the Storage and then link stored file for the Draft. - - name: Submission - - name: TwoFactorAuth - - name: Users - - name: Webhooks\n`; + let docs = { + openapi: '3.0.0', + info: { + title: 'WildDuck API', + description: 'WildDuck API docs', + version: '1.0.0', + contact: { + url: 'https://github.com/nodemailer/wildduck' + } + }, + servers: [{ url: 'https://api.wildduck.email' }], + tags: [ + { name: 'Addresses' }, + { name: 'ApplicationPasswords' }, + { + name: 'Archive', + description: + 'Archive includes all deleted messages. Once messages are old enough then these are permanenetly deleted from the archive as well. Until then you can restore the deleted messages.' + }, + { + name: 'Audit', + description: + 'Auditing allows to monitor an email account. All existing, deleted and new emails are copied to the auditing system. See also https://github.com/nodemailer/wildduck-audit-manager' + }, + { name: 'Authentication' }, + { name: 'Autoreplies' }, + { + name: 'Certs', + description: + 'WildDuck allows to register TLS certificates to be used with SNI connections. These certificates are used by IMAP, POP3, API and SMTP servers when a SNI capable client establishes a TLS connection. This does not apply for MX servers.' + }, + { + name: 'DKIM', + description: + 'Whenever an email is sent WildDuck checks if there is a DKIM key registered for the domain name of the sender address and uses it to sign the message.' + }, + { + name: 'DomainAccess', + description: 'Add sender domain names to allowlist (messages are all accepted) or blocklist (messages are sent to Spam folder)' + }, + { name: 'DomainAliases' }, + { name: 'Filters' }, + { name: 'Mailboxes' }, + { name: 'Messages' }, + { name: 'Settings' }, + { + name: 'Storage', + description: + 'Storage allows easier attachment handling when composing Draft messages. Instead of uploading the attachmnent with every draft update, you store the attachment to the Storage and then link stored file for the Draft.' + }, + { name: 'Submission' }, + { name: 'TwoFactorAuth' }, + { name: 'Users' }, + { name: 'Webhooks' } + ] + }; + const mapPathToMethods = {}; // map -> {path -> {post -> {}, put -> {}, delete -> {}, get -> {}}} for (const routePath in routes) { @@ -1039,30 +1058,28 @@ tags: const finalObj = { paths: mapPathToMethods }; - const mapPathToMethodsYaml = yaml.dump(finalObj, { indent: 4, lineWidth: -1, noRefs: true }); - const componentsYaml = yaml.dump(components, { indent: 4, lineWidth: -1, noRefs: true }); - - docs += mapPathToMethodsYaml; - docs += componentsYaml; - - docs += ` -securitySchemes: - AccessTokenAuth: - name: X-Access-Token - type: apiKey - in: header - description: |- - If authentication is enabled in the WildDuck configuration, you will need to supply an access token in the \`X-Access-Token\` header. - - \`\`\`json - { - "X-Access-Token": "59fc66a03e54454869460e45" - } - \`\`\` -security: -- AccessTokenAuth: [] -`; + docs = { ...docs, ...finalObj }; + docs = { ...docs, ...components }; + + docs = { + ...docs, + securitySchemes: { + AccessTokenAuth: { + name: 'X-Access-Token', + type: 'apiKey', + in: 'header', + description: `If authentication is enabled in the WildDuck configuration, you will need to supply an access token in the \`X-Access-Token\` header. + \`\`\`json + { + "X-Access-Token": "59fc66a03e54454869460e45" + } + \`\`\` + ` + } + }, + security: [{ AccessTokenAuth: [] }] + }; - await fs.promises.writeFile(__dirname + '/../openapidocs.yml', docs); + await fs.promises.writeFile(__dirname + '/../openapidocs.json', JSON.stringify(docs)); } };