diff --git a/imap-core/lib/indexer/body-structure.js b/imap-core/lib/indexer/body-structure.js index 4b7110b1..d9aab464 100644 --- a/imap-core/lib/indexer/body-structure.js +++ b/imap-core/lib/indexer/body-structure.js @@ -4,7 +4,6 @@ const libmime = require('libmime'); const createEnvelope = require('./create-envelope'); class BodyStructure { - constructor(tree, options) { this.tree = tree; this.options = options || {}; @@ -38,13 +37,10 @@ class BodyStructure { case 'text': return this.processTextNode(node, options); case 'message': - if (node.parsedHeader['content-type'].subtype === 'rfc822') { - if (!options.attachmentRFC822) { - return this.processRFC822Node(node, options); - } - return this.processAttachmentNode(node, options); + if (node.parsedHeader['content-type'].subtype === 'rfc822' && node.message && !options.attachmentRFC822) { + return this.processRFC822Node(node, options); } - // fall through + // fall through default: return this.processAttachmentNode(node, options); } @@ -60,32 +56,32 @@ class BodyStructure { * @return {Array} A list of basic fields */ getBasicFields(node, options) { - let bodyType = node.parsedHeader['content-type'] && node.parsedHeader['content-type'].type || null; - let bodySubtype = node.parsedHeader['content-type'] && node.parsedHeader['content-type'].subtype || null; + let bodyType = (node.parsedHeader['content-type'] && node.parsedHeader['content-type'].type) || null; + let bodySubtype = (node.parsedHeader['content-type'] && node.parsedHeader['content-type'].subtype) || null; let contentTransfer = node.parsedHeader['content-transfer-encoding'] || '7bit'; return [ // body type - options.upperCaseKeys ? bodyType && bodyType.toUpperCase() || null : bodyType, + options.upperCaseKeys ? (bodyType && bodyType.toUpperCase()) || null : bodyType, // body subtype - options.upperCaseKeys ? bodySubtype && bodySubtype.toUpperCase() || null : bodySubtype, + options.upperCaseKeys ? (bodySubtype && bodySubtype.toUpperCase()) || null : bodySubtype, // body parameter parenthesized list - node.parsedHeader['content-type'] && - node.parsedHeader['content-type'].hasParams && - this.flatten(Object.keys(node.parsedHeader['content-type'].params).map(key => { - let value = node.parsedHeader['content-type'].params[key]; - try { - value = Buffer.from(libmime.decodeWords(value).trim()); - } catch (E) { - // failed to parse value - } - return [ - options.upperCaseKeys ? key.toUpperCase() : key, - value - ]; - })) || null, + (node.parsedHeader['content-type'] && + node.parsedHeader['content-type'].hasParams && + this.flatten( + Object.keys(node.parsedHeader['content-type'].params).map(key => { + let value = node.parsedHeader['content-type'].params[key]; + try { + value = Buffer.from(libmime.decodeWords(value).trim()); + } catch (E) { + // failed to parse value + } + return [options.upperCaseKeys ? key.toUpperCase() : key, value]; + }) + )) || + null, // body id node.parsedHeader['content-id'] || null, @@ -94,7 +90,7 @@ class BodyStructure { node.parsedHeader['content-description'] || null, // body encoding - options.upperCaseKeys ? contentTransfer && contentTransfer.toUpperCase() || '7bit' : contentTransfer, + options.upperCaseKeys ? (contentTransfer && contentTransfer.toUpperCase()) || '7bit' : contentTransfer, // body size node.size @@ -111,9 +107,8 @@ class BodyStructure { getExtensionFields(node, options) { options = options || {}; - let languageString = node.parsedHeader['content-language'] && - node.parsedHeader['content-language'].replace(/[ ,]+/g, ',').replace(/^,+|,+$/g, ''); - let language = languageString && languageString.split(',') || null; + let languageString = node.parsedHeader['content-language'] && node.parsedHeader['content-language'].replace(/[ ,]+/g, ',').replace(/^,+|,+$/g, ''); + let language = (languageString && languageString.split(',')) || null; let data; // if `contentLanguageString` is true, then use a string instead of single element array @@ -126,25 +121,24 @@ class BodyStructure { node.parsedHeader['content-md5'] || null, // body disposition - node.parsedHeader['content-disposition'] && [ - options.upperCaseKeys ? - node.parsedHeader['content-disposition'].value.toUpperCase() : - node.parsedHeader['content-disposition'].value, - node.parsedHeader['content-disposition'].params && - node.parsedHeader['content-disposition'].hasParams && - this.flatten(Object.keys(node.parsedHeader['content-disposition'].params).map(key => { - let value = node.parsedHeader['content-disposition'].params[key]; - try { - value = Buffer.from(libmime.decodeWords(value).trim()); - } catch (E) { - // failed to parse value - } - return [ - options.upperCaseKeys ? key.toUpperCase() : key, - value - ]; - })) || null - ] || null, + (node.parsedHeader['content-disposition'] && [ + options.upperCaseKeys ? node.parsedHeader['content-disposition'].value.toUpperCase() : node.parsedHeader['content-disposition'].value, + (node.parsedHeader['content-disposition'].params && + node.parsedHeader['content-disposition'].hasParams && + this.flatten( + Object.keys(node.parsedHeader['content-disposition'].params).map(key => { + let value = node.parsedHeader['content-disposition'].params[key]; + try { + value = Buffer.from(libmime.decodeWords(value).trim()); + } catch (E) { + // failed to parse value + } + return [options.upperCaseKeys ? key.toUpperCase() : key, value]; + }) + )) || + null + ]) || + null, // body language language @@ -174,36 +168,35 @@ class BodyStructure { processMultipartNode(node, options) { options = options || {}; - let data = (node.childNodes && node.childNodes.map(tree => this.createBodystructure(tree, options)) || [ - [] - ]). - concat([ + let data = ((node.childNodes && node.childNodes.map(tree => this.createBodystructure(tree, options))) || [[]]).concat([ // body subtype - options.upperCaseKeys ? node.multipart && node.multipart.toUpperCase() || null : node.multipart, + options.upperCaseKeys ? (node.multipart && node.multipart.toUpperCase()) || null : node.multipart, // body parameter parenthesized list - node.parsedHeader['content-type'] && - node.parsedHeader['content-type'].hasParams && - this.flatten(Object.keys(node.parsedHeader['content-type'].params).map(key => { - let value = node.parsedHeader['content-type'].params[key]; - try { - value = Buffer.from(libmime.decodeWords(value).trim()); - } catch (E) { - // failed to parse value - } - return [ - options.upperCaseKeys ? key.toUpperCase() : key, - value - ]; - })) || null + (node.parsedHeader['content-type'] && + node.parsedHeader['content-type'].hasParams && + this.flatten( + Object.keys(node.parsedHeader['content-type'].params).map(key => { + let value = node.parsedHeader['content-type'].params[key]; + try { + value = Buffer.from(libmime.decodeWords(value).trim()); + } catch (E) { + // failed to parse value + } + return [options.upperCaseKeys ? key.toUpperCase() : key, value]; + }) + )) || + null ]); if (options.body) { return data; } else { - return data. - // skip body MD5 from extension fields - concat(this.getExtensionFields(node, options).slice(1)); + return ( + data + // skip body MD5 from extension fields + .concat(this.getExtensionFields(node, options).slice(1)) + ); } } @@ -217,9 +210,7 @@ class BodyStructure { processTextNode(node, options) { options = options || {}; - let data = [].concat(this.getBasicFields(node, options)).concat([ - node.lineCount - ]); + let data = [].concat(this.getBasicFields(node, options)).concat([node.lineCount]); if (!options.body) { data = data.concat(this.getExtensionFields(node, options)); @@ -261,10 +252,7 @@ class BodyStructure { data.push(createEnvelope(node.message.parsedHeader)); data.push(this.createBodystructure(node.message, options)); - data = data.concat( - node.lineCount - ). - concat(this.getExtensionFields(node, options)); + data = data.concat(node.lineCount).concat(this.getExtensionFields(node, options)); return data; } @@ -291,7 +279,6 @@ class BodyStructure { } return result; } - } // Expose to the world diff --git a/imap-core/lib/indexer/create-envelope.js b/imap-core/lib/indexer/create-envelope.js index 13dd818d..69d8fa50 100644 --- a/imap-core/lib/indexer/create-envelope.js +++ b/imap-core/lib/indexer/create-envelope.js @@ -11,8 +11,7 @@ const punycode = require('punycode'); * @param {Object} message A parsed mime tree node * @return {Object} ENVELOPE compatible object */ -module.exports = function (header) { - +module.exports = function(header) { let subject = Array.isArray(header.subject) ? header.subject.reverse().filter(line => line.trim()) : header.subject; subject = Buffer.from(subject || '', 'binary').toString(); @@ -78,9 +77,7 @@ function processAddress(arr, defaults) { domain = Buffer.from(punycode.toUnicode(domain)); } - result.push([ - name, null, user, domain - ]); + result.push([name, null, user, domain]); } else { // Handle group syntax let name = addr.name || ''; diff --git a/imap-core/lib/indexer/indexer.js b/imap-core/lib/indexer/indexer.js index d2c85441..b8507148 100644 --- a/imap-core/lib/indexer/indexer.js +++ b/imap-core/lib/indexer/indexer.js @@ -15,6 +15,7 @@ const iconv = require('iconv-lite'); const he = require('he'); const htmlToText = require('html-to-text'); const crypto = require('crypto'); + let cryptoAsync; try { cryptoAsync = require('@ronomon/crypto-async'); // eslint-disable-line global-require @@ -23,7 +24,6 @@ try { } class Indexer { - constructor(options) { this.options = options || {}; this.fetchOptions = this.options.fetchOptions || {}; @@ -67,7 +67,6 @@ class Indexer { }; let walk = (node, next) => { - if (!textOnly || !root) { append(formatHeaders(node.header).join('\r\n') + '\r\n'); } @@ -157,7 +156,6 @@ class Indexer { }; let walk = (node, next) => { - if (aborted) { return next(); } @@ -234,9 +232,11 @@ class Indexer { } }; - setImmediate(walk.bind(null, mimeTree, () => { - res.end(); - })); + setImmediate( + walk.bind(null, mimeTree, () => { + res.end(); + }) + ); // if called then stops resolving rest of the message res.abort = () => { @@ -290,7 +290,9 @@ class Indexer { let parsedDisposition = node.parsedHeader['content-disposition']; let transferEncoding = (node.parsedHeader['content-transfer-encoding'] || '7bit').toLowerCase().trim(); - let contentType = (parsedContentType && parsedContentType.value || (node.rootNode ? 'text/plain' : 'application/octet-stream')).toLowerCase().trim(); + let contentType = ((parsedContentType && parsedContentType.value) || (node.rootNode ? 'text/plain' : 'application/octet-stream')) + .toLowerCase() + .trim(); alternative = alternative || contentType === 'multipart/alternative'; related = related || contentType === 'multipart/related'; @@ -302,13 +304,16 @@ class Indexer { } } - let disposition = (parsedDisposition && parsedDisposition.value || '').toLowerCase().trim() || false; + let disposition = ((parsedDisposition && parsedDisposition.value) || '').toLowerCase().trim() || false; let isInlineText = false; let isMultipart = contentType.split('/')[0] === 'multipart'; // If the current node is HTML or Plaintext then allow larger content included in the mime tree // Also decode text/html value - if (['text/plain', 'text/html', 'text/rfc822-headers', 'message/delivery-status'].includes(contentType) && (!disposition || disposition === 'inline')) { + if ( + ['text/plain', 'text/html', 'text/rfc822-headers', 'message/delivery-status'].includes(contentType) && + (!disposition || disposition === 'inline') + ) { isInlineText = true; if (node.body && node.body.length) { let charset = parsedContentType.params.charset || 'windows-1257'; @@ -353,7 +358,12 @@ class Indexer { let attachmentId = 'ATT' + leftPad(++idcount, '0', 5); map[attachmentId] = new ObjectID(); - let fileName = (node.parsedHeader['content-disposition'] && node.parsedHeader['content-disposition'].params && node.parsedHeader['content-disposition'].params.filename) || (node.parsedHeader['content-type'] && node.parsedHeader['content-type'].params && node.parsedHeader['content-type'].params.name) || false; + let fileName = + (node.parsedHeader['content-disposition'] && + node.parsedHeader['content-disposition'].params && + node.parsedHeader['content-disposition'].params.filename) || + (node.parsedHeader['content-type'] && node.parsedHeader['content-type'].params && node.parsedHeader['content-type'].params.name) || + false; let contentId = (node.parsedHeader['content-id'] || '').toString().replace(/<|>/g, '').trim(); if (fileName) { @@ -363,7 +373,7 @@ class Indexer { // failed to parse filename, keep as is (most probably an unknown charset is used) } } else { - fileName = (crypto.randomBytes(4).toString('hex') + '.' + libmime.detectExtension(contentType)); + fileName = crypto.randomBytes(4).toString('hex') + '.' + libmime.detectExtension(contentType); } cidMap.set(contentId, { @@ -425,13 +435,14 @@ class Indexer { walk(mimeTree, false, false); - let updateCidLinks = str => str.replace(/\bcid:([^\s"']+)/g, (match, cid) => { - if (cidMap.has(cid)) { - let attachment = cidMap.get(cid); - return 'attachment:' + messageId + '/' + attachment.id.toString(); - } - return match; - }); + let updateCidLinks = str => + str.replace(/\bcid:([^\s"']+)/g, (match, cid) => { + if (cidMap.has(cid)) { + let attachment = cidMap.get(cid); + return 'attachment:' + messageId + '/' + attachment.id.toString(); + } + return match; + }); maildata.html = htmlContent.filter(str => str.trim()).map(updateCidLinks); maildata.text = textContent.filter(str => str.trim()).map(updateCidLinks).join('\n').trim(); @@ -447,10 +458,8 @@ class Indexer { let nodes = maildata.nodes; let storeNode = () => { if (pos >= nodes.length) { - // replace attachment IDs with ObjectIDs in the mimeTree let walk = (node, next) => { - if (node.attachmentId && maildata.map[node.attachmentId]) { node.attachmentId = maildata.map[node.attachmentId]; } @@ -546,7 +555,6 @@ class Indexer { * @return {Array} BODY object as a structured Array */ getBody(mimeTree) { - // BODY – BODYSTRUCTURE without extension data let body = new BodyStructure(mimeTree, { upperCaseKeys: true, @@ -563,7 +571,6 @@ class Indexer { * @return {Array} BODYSTRUCTURE object as a structured Array */ getBodyStructure(mimeTree) { - // full BODYSTRUCTURE let bodystructure = new BodyStructure(mimeTree, { upperCaseKeys: true, @@ -647,7 +654,6 @@ class Indexer { sent = true; return callback(null, Buffer.concat(buffers, buflen)); }); - } else { return setImmediate(() => callback(null, Buffer.from((data || '').toString(), 'binary'))); } @@ -711,20 +717,28 @@ class Indexer { if (!selector.headers || !selector.headers.length) { return '\r\n\r\n'; } - return formatHeaders(node.header).filter(line => { - let key = line.split(':').shift().toLowerCase().trim(); - return selector.headers.indexOf(key) >= 0; - }).join('\r\n') + '\r\n\r\n'; + return ( + formatHeaders(node.header) + .filter(line => { + let key = line.split(':').shift().toLowerCase().trim(); + return selector.headers.indexOf(key) >= 0; + }) + .join('\r\n') + '\r\n\r\n' + ); case 'header.fields.not': // BODY[HEADER.FIELDS.NOT (Key1 Key2 KeyN)] all but selected header keys if (!selector.headers || !selector.headers.length) { return formatHeaders(node.header).join('\r\n') + '\r\n\r\n'; } - return formatHeaders(node.header).filter(line => { - let key = line.split(':').shift().toLowerCase().trim(); - return selector.headers.indexOf(key) < 0; - }).join('\r\n') + '\r\n\r\n'; + return ( + formatHeaders(node.header) + .filter(line => { + let key = line.split(':').shift().toLowerCase().trim(); + return selector.headers.indexOf(key) < 0; + }) + .join('\r\n') + '\r\n\r\n' + ); case 'mime': // BODY[1.2.3.MIME] mime node header @@ -754,19 +768,21 @@ function formatHeaders(headers) { return headers; } - function textToHtml(str) { - - let text = '

' + he. - // encode special chars - encode( - str, { - useNamedReferences: true - }). - replace(/\r?\n/g, '\n').trim(). // normalize line endings - replace(/[ \t]+$/mg, '').trim(). // trim empty line endings - replace(/\n\n+/g, '

').trim(). // insert

to multiple linebreaks - replace(/\n/g, '
') + // insert
to single linebreaks + let text = + '

' + + he + // encode special chars + .encode(str, { + useNamedReferences: true + }) + .replace(/\r?\n/g, '\n') + .trim() // normalize line endings + .replace(/[ \t]+$/gm, '') + .trim() // trim empty line endings + .replace(/\n\n+/g, '

') + .trim() // insert

to multiple linebreaks + .replace(/\n/g, '
') + // insert
to single linebreaks '

'; return text; diff --git a/imap-core/lib/indexer/parse-mime-tree.js b/imap-core/lib/indexer/parse-mime-tree.js index 24afb44a..ed5b61dc 100644 --- a/imap-core/lib/indexer/parse-mime-tree.js +++ b/imap-core/lib/indexer/parse-mime-tree.js @@ -1,6 +1,6 @@ 'use strict'; -let addressparser = require('nodemailer/lib/addressparser'); +const addressparser = require('nodemailer/lib/addressparser'); /** * Parses a RFC822 message into a structured object (JSON compatible) @@ -9,9 +9,7 @@ let addressparser = require('nodemailer/lib/addressparser'); * @param {String|Buffer} rfc822 Raw body of the message */ class MIMEParser { - constructor(rfc822) { - // ensure the input is a binary string this.rfc822 = (rfc822 || '').toString('binary'); @@ -38,7 +36,6 @@ class MIMEParser { line = this.readLine(); switch (this._node.state) { - case 'header': // process header section if (this.rawBody) { this.rawBody += prevBr + line; @@ -58,8 +55,11 @@ class MIMEParser { this.rawBody += prevBr + line; if (this._node.parentBoundary && (line === '--' + this._node.parentBoundary || line === '--' + this._node.parentBoundary + '--')) { - - if (this._node.parsedHeader['content-type'].value === 'message/rfc822') { + if ( + this._node.parsedHeader['content-type'].value === 'message/rfc822' && + (!this._node.parsedHeader['content-transfer-encoding'] || + ['7bit', '8bit', 'binary'].includes(this._node.parsedHeader['content-transfer-encoding'])) + ) { this._node.message = parse(this._node.body.join('')); } @@ -78,7 +78,8 @@ class MIMEParser { } break; - default: // never should be reached + default: + // never should be reached throw new Error('Unexpected state'); } @@ -107,7 +108,6 @@ class MIMEParser { * from the tree (circular references prohibit conversion to JSON) */ finalizeTree() { - if (this._node.state === 'header') { this.processNodeHeader(); this.processContentType(); @@ -125,9 +125,12 @@ class MIMEParser { node.lineCount = node.body.length; node.body = Buffer.from( - node.body.join(''). - // ensure proper line endings - replace(/\r?\n/g, '\r\n'), 'binary'); + node.body + .join('') + // ensure proper line endings + .replace(/\r?\n/g, '\r\n'), + 'binary' + ); node.size = node.body.length; } node.childNodes.forEach(walker); @@ -243,7 +246,8 @@ class MIMEParser { subtype: '', params: {} }, - match, processEncodedWords = {}; + match, + processEncodedWords = {}; (headerValue || '').split(';').forEach((part, i) => { let key, value; diff --git a/imap.js b/imap.js index 0f0a771f..69712fef 100644 --- a/imap.js +++ b/imap.js @@ -69,67 +69,93 @@ let userHandler; let gcTimeout; let gcLock; -server.onAuth = function (login, session, callback) { +server.onAuth = function(login, session, callback) { let username = (login.username || '').toString().trim(); - userHandler.authenticate(username, login.password, { - protocol: 'IMAP', - ip: session.remoteAddress - }, (err, result) => { - if (err) { - return callback(err); - } - if (!result) { - return callback(); - } - - if (result.scope === 'master' && result.enabled2fa) { - // master password not allowed if 2fa is enabled! - return callback(); - } - - callback(null, { - user: { - id: result.user, - username: result.username + userHandler.authenticate( + username, + login.password, + { + protocol: 'IMAP', + ip: session.remoteAddress + }, + (err, result) => { + if (err) { + return callback(err); } - }); - }); + if (!result) { + return callback(); + } + + if (result.scope === 'master' && result.enabled2fa) { + // master password not allowed if 2fa is enabled! + return callback(); + } + + callback(null, { + user: { + id: result.user, + username: result.username + } + }); + } + ); }; // LIST "" "*" // Returns all folders, query is informational // folders is either an Array or a Map -server.onList = function (query, session, callback) { - this.logger.debug({ - tnx: 'list', - cid: session.id - }, '[%s] LIST for "%s"', session.id, query); - db.database.collection('mailboxes').find({ - user: session.user.id - }).toArray(callback); +server.onList = function(query, session, callback) { + this.logger.debug( + { + tnx: 'list', + cid: session.id + }, + '[%s] LIST for "%s"', + session.id, + query + ); + db.database + .collection('mailboxes') + .find({ + user: session.user.id + }) + .toArray(callback); }; // LSUB "" "*" // Returns all subscribed folders, query is informational // folders is either an Array or a Map -server.onLsub = function (query, session, callback) { - this.logger.debug({ - tnx: 'lsub', - cid: session.id - }, '[%s] LSUB for "%s"', session.id, query); - db.database.collection('mailboxes').find({ - user: session.user.id, - subscribed: true - }).toArray(callback); +server.onLsub = function(query, session, callback) { + this.logger.debug( + { + tnx: 'lsub', + cid: session.id + }, + '[%s] LSUB for "%s"', + session.id, + query + ); + db.database + .collection('mailboxes') + .find({ + user: session.user.id, + subscribed: true + }) + .toArray(callback); }; // SUBSCRIBE "path/to/mailbox" -server.onSubscribe = function (path, session, callback) { - this.logger.debug({ - tnx: 'subscribe', - cid: session.id - }, '[%s] SUBSCRIBE to "%s"', session.id, path); +server.onSubscribe = function(path, session, callback) { + this.logger.debug( + { + tnx: 'subscribe', + cid: session.id + }, + '[%s] SUBSCRIBE to "%s"', + session.id, + path + ); db.database.collection('mailboxes').findOneAndUpdate({ user: session.user.id, path @@ -152,11 +178,16 @@ server.onSubscribe = function (path, session, callback) { }; // UNSUBSCRIBE "path/to/mailbox" -server.onUnsubscribe = function (path, session, callback) { - this.logger.debug({ - tnx: 'unsubscribe', - cid: session.id - }, '[%s] UNSUBSCRIBE from "%s"', session.id, path); +server.onUnsubscribe = function(path, session, callback) { + this.logger.debug( + { + tnx: 'unsubscribe', + cid: session.id + }, + '[%s] UNSUBSCRIBE from "%s"', + session.id, + path + ); db.database.collection('mailboxes').findOneAndUpdate({ user: session.user.id, path @@ -179,11 +210,16 @@ server.onUnsubscribe = function (path, session, callback) { }; // CREATE "path/to/mailbox" -server.onCreate = function (path, session, callback) { - this.logger.debug({ - tnx: 'create', - cid: session.id - }, '[%s] CREATE "%s"', session.id, path); +server.onCreate = function(path, session, callback) { + this.logger.debug( + { + tnx: 'create', + cid: session.id + }, + '[%s] CREATE "%s"', + session.id, + path + ); db.database.collection('mailboxes').findOne({ user: session.user.id, path @@ -229,11 +265,17 @@ server.onCreate = function (path, session, callback) { // RENAME "path/to/mailbox" "new/path" // NB! RENAME affects child and hierarchy mailboxes as well, this example does not do this -server.onRename = function (path, newname, session, callback) { - this.logger.debug({ - tnx: 'rename', - cid: session.id - }, '[%s] RENAME "%s" to "%s"', session.id, path, newname); +server.onRename = function(path, newname, session, callback) { + this.logger.debug( + { + tnx: 'rename', + cid: session.id + }, + '[%s] RENAME "%s" to "%s"', + session.id, + path, + newname + ); db.database.collection('mailboxes').findOne({ user: session.user.id, path: newname @@ -268,11 +310,16 @@ server.onRename = function (path, newname, session, callback) { }; // DELETE "path/to/mailbox" -server.onDelete = function (path, session, callback) { - this.logger.debug({ - tnx: 'delete', - cid: session.id - }, '[%s] DELETE "%s"', session.id, path); +server.onDelete = function(path, session, callback) { + this.logger.debug( + { + tnx: 'delete', + cid: session.id + }, + '[%s] DELETE "%s"', + session.id, + path + ); db.database.collection('mailboxes').findOne({ user: session.user.id, path @@ -295,72 +342,90 @@ server.onDelete = function (path, session, callback) { } // calculate mailbox size by aggregating the size's of all messages - db.database.collection('messages').aggregate([{ - $match: { - mailbox: mailbox._id - } - }, { - $group: { - _id: { - mailbox: '$mailbox' + db.database + .collection('messages') + .aggregate( + [ + { + $match: { + mailbox: mailbox._id + } }, - storageUsed: { - $sum: '$size' + { + $group: { + _id: { + mailbox: '$mailbox' + }, + storageUsed: { + $sum: '$size' + } + } + } + ], + { + cursor: { + batchSize: 1 } } - }], { - cursor: { - batchSize: 1 - } - }).toArray((err, res) => { - if (err) { - return callback(err); - } - - let storageUsed = res && res[0] && res[0].storageUsed || 0; - - db.database.collection('messages').deleteMany({ - mailbox: mailbox._id - }, err => { + ) + .toArray((err, res) => { if (err) { return callback(err); } - let done = () => { - db.database.collection('journal').deleteMany({ - mailbox: mailbox._id - }, err => { - if (err) { - return callback(err); - } - callback(null, true); - }); - }; + let storageUsed = (res && res[0] && res[0].storageUsed) || 0; - if (!storageUsed) { - return done(); - } - - // decrement quota counters - db.database.collection('users').findOneAndUpdate({ - _id: mailbox.user - }, { - $inc: { - storageUsed: -Number(storageUsed) || 0 + db.database.collection('messages').deleteMany({ + mailbox: mailbox._id + }, err => { + if (err) { + return callback(err); } - }, done); + + let done = () => { + db.database.collection('journal').deleteMany({ + mailbox: mailbox._id + }, err => { + if (err) { + return callback(err); + } + callback(null, true); + }); + }; + + if (!storageUsed) { + return done(); + } + + // decrement quota counters + db.database.collection('users').findOneAndUpdate( + { + _id: mailbox.user + }, + { + $inc: { + storageUsed: -Number(storageUsed) || 0 + } + }, + done + ); + }); }); - }); }); }); }; // SELECT/EXAMINE -server.onOpen = function (path, session, callback) { - this.logger.debug({ - tnx: 'open', - cid: session.id - }, '[%s] Opening "%s"', session.id, path); +server.onOpen = function(path, session, callback) { + this.logger.debug( + { + tnx: 'open', + cid: session.id + }, + '[%s] Opening "%s"', + session.id, + path + ); db.database.collection('mailboxes').findOne({ user: session.user.id, path @@ -372,28 +437,36 @@ server.onOpen = function (path, session, callback) { return callback(null, 'NONEXISTENT'); } - db.database.collection('messages').find({ - mailbox: mailbox._id - }).project({ - uid: true - }).sort([ - ['uid', 1] - ]).toArray((err, messages) => { - if (err) { - return callback(err); - } - mailbox.uidList = messages.map(message => message.uid); - callback(null, mailbox); - }); + db.database + .collection('messages') + .find({ + mailbox: mailbox._id + }) + .project({ + uid: true + }) + .sort([['uid', 1]]) + .toArray((err, messages) => { + if (err) { + return callback(err); + } + mailbox.uidList = messages.map(message => message.uid); + callback(null, mailbox); + }); }); }; // STATUS (X Y X) -server.onStatus = function (path, session, callback) { - this.logger.debug({ - tnx: 'status', - cid: session.id - }, '[%s] Requested status for "%s"', session.id, path); +server.onStatus = function(path, session, callback) { + this.logger.debug( + { + tnx: 'status', + cid: session.id + }, + '[%s] Requested status for "%s"', + session.id, + path + ); db.database.collection('mailboxes').findOne({ user: session.user.id, path @@ -405,38 +478,48 @@ server.onStatus = function (path, session, callback) { return callback(null, 'NONEXISTENT'); } - db.database.collection('messages').find({ - mailbox: mailbox._id - }).count((err, total) => { - if (err) { - return callback(err); - } - db.database.collection('messages').find({ - mailbox: mailbox._id, - seen: false - }).count((err, unseen) => { + db.database + .collection('messages') + .find({ + mailbox: mailbox._id + }) + .count((err, total) => { if (err) { return callback(err); } + db.database + .collection('messages') + .find({ + mailbox: mailbox._id, + seen: false + }) + .count((err, unseen) => { + if (err) { + return callback(err); + } - return callback(null, { - messages: total, - uidNext: mailbox.uidNext, - uidValidity: mailbox.uidValidity, - unseen - }); + return callback(null, { + messages: total, + uidNext: mailbox.uidNext, + uidValidity: mailbox.uidValidity, + unseen + }); + }); }); - }); - }); }; // APPEND mailbox (flags) date message -server.onAppend = function (path, flags, date, raw, session, callback) { - this.logger.debug({ - tnx: 'append', - cid: session.id - }, '[%s] Appending message to "%s"', session.id, path); +server.onAppend = function(path, flags, date, raw, session, callback) { + this.logger.debug( + { + tnx: 'append', + cid: session.id + }, + '[%s] Appending message to "%s"', + session.id, + path + ); db.database.collection('users').findOne({ _id: session.user.id @@ -452,31 +535,34 @@ server.onAppend = function (path, flags, date, raw, session, callback) { return callback(false, 'OVERQUOTA'); } - messageHandler.add({ - user: session.user.id, - path, - meta: { - source: 'IMAP', - to: session.user.username, - time: Date.now() + messageHandler.add( + { + user: session.user.id, + path, + meta: { + source: 'IMAP', + to: session.user.username, + time: Date.now() + }, + session, + date, + flags, + raw }, - session, - date, - flags, - raw - }, (err, status, data) => { - if (err) { - if (err.imapResponse) { - return callback(null, err.imapResponse); + (err, status, data) => { + if (err) { + if (err.imapResponse) { + return callback(null, err.imapResponse); + } + return callback(err); } - return callback(err); + callback(null, status, data); } - callback(null, status, data); - }); + ); }); }; -server.updateMailboxFlags = function (mailbox, update, callback) { +server.updateMailboxFlags = function(mailbox, update, callback) { if (update.action === 'remove') { // we didn't add any new flags, so there's nothing to update return callback(); @@ -504,23 +590,33 @@ server.updateMailboxFlags = function (mailbox, update, callback) { // found some new flags not yet set for mailbox // FIXME: Should we send unsolicited FLAGS and PERMANENTFLAGS notifications? Probably not - return db.database.collection('mailboxes').findOneAndUpdate({ - _id: mailbox._id - }, { - $addToSet: { - flags: { - $each: newFlags + return db.database.collection('mailboxes').findOneAndUpdate( + { + _id: mailbox._id + }, + { + $addToSet: { + flags: { + $each: newFlags + } } - } - }, {}, callback); + }, + {}, + callback + ); }; // STORE / UID STORE, updates flags for selected UIDs -server.onStore = function (path, update, session, callback) { - this.logger.debug({ - tnx: 'store', - cid: session.id - }, '[%s] Updating messages in "%s"', session.id, path); +server.onStore = function(path, update, session, callback) { + this.logger.debug( + { + tnx: 'store', + cid: session.id + }, + '[%s] Updating messages in "%s"', + session.id, + path + ); db.database.collection('mailboxes').findOne({ user: session.user.id, path @@ -557,15 +653,15 @@ server.onStore = function (path, update, session, callback) { queryAll = true; } - let cursor = db.database.collection('messages'). - find(query). - project({ - _id: true, - uid: true, - flags: true - }).sort([ - ['uid', 1] - ]); + let cursor = db.database + .collection('messages') + .find(query) + .project({ + _id: true, + uid: true, + flags: true + }) + .sort([['uid', 1]]); let updateEntries = []; let notifyEntries = []; @@ -580,7 +676,8 @@ server.onStore = function (path, update, session, callback) { this.notifier.addEntries(session.user.id, path, notifyEntries, () => { notifyEntries = []; this.notifier.fire(session.user.id, path); - if (args[0]) { // first argument is an error + if (args[0]) { + // first argument is an error return callback(...args); } else { server.updateMailboxFlags(mailbox, update, () => callback(...args)); @@ -589,7 +686,8 @@ server.onStore = function (path, update, session, callback) { }); } this.notifier.fire(session.user.id, path); - if (args[0]) { // first argument is an error + if (args[0]) { + // first argument is an error return callback(...args); } else { server.updateMailboxFlags(mailbox, update, () => callback(...args)); @@ -644,112 +742,124 @@ server.onStore = function (path, update, session, callback) { } break; - case 'add': - { - let newFlags = []; - message.flags = message.flags.concat(update.value.filter(flag => { + case 'add': { + let newFlags = []; + message.flags = message.flags.concat( + update.value.filter(flag => { if (!existingFlags.includes(flag.toLowerCase().trim())) { updated = true; newFlags.push(flag); return true; } return false; - })); + }) + ); - // add flags - if (updated) { - flagsupdate = { - $addToSet: { - flags: { - $each: newFlags - } + // add flags + if (updated) { + flagsupdate = { + $addToSet: { + flags: { + $each: newFlags } - }; + } + }; - if (newFlags.includes('\\Seen') || newFlags.includes('\\Flagged') || newFlags.includes('\\Deleted') || newFlags.includes('\\Draft')) { - flagsupdate.$set = {}; - if (newFlags.includes('\\Seen')) { - flagsupdate.$set = { - seen: true - }; - } - if (newFlags.includes('\\Flagged')) { - flagsupdate.$set = { - flagged: true - }; - } - if (newFlags.includes('\\Deleted')) { - flagsupdate.$set = { - deleted: true - }; - } - if (newFlags.includes('\\Draft')) { - flagsupdate.$set = { - draft: true - }; - } + if ( + newFlags.includes('\\Seen') || + newFlags.includes('\\Flagged') || + newFlags.includes('\\Deleted') || + newFlags.includes('\\Draft') + ) { + flagsupdate.$set = {}; + if (newFlags.includes('\\Seen')) { + flagsupdate.$set = { + seen: true + }; + } + if (newFlags.includes('\\Flagged')) { + flagsupdate.$set = { + flagged: true + }; + } + if (newFlags.includes('\\Deleted')) { + flagsupdate.$set = { + deleted: true + }; + } + if (newFlags.includes('\\Draft')) { + flagsupdate.$set = { + draft: true + }; } } - break; } + break; + } - case 'remove': - { - // We need to use the case of existing flags when removing - let oldFlags = []; - let flagsUpdates = update.value.map(flag => flag.toLowerCase().trim()); - message.flags = message.flags.filter(flag => { - if (!flagsUpdates.includes(flag.toLowerCase().trim())) { - return true; + case 'remove': { + // We need to use the case of existing flags when removing + let oldFlags = []; + let flagsUpdates = update.value.map(flag => flag.toLowerCase().trim()); + message.flags = message.flags.filter(flag => { + if (!flagsUpdates.includes(flag.toLowerCase().trim())) { + return true; + } + oldFlags.push(flag); + updated = true; + return false; + }); + + // remove flags + if (updated) { + flagsupdate = { + $pull: { + flags: { + $in: oldFlags + } } - oldFlags.push(flag); - updated = true; - return false; - }); - - // remove flags - if (updated) { - flagsupdate = { - $pull: { - flags: { - $in: oldFlags - } - } - }; - if (oldFlags.includes('\\Seen') || oldFlags.includes('\\Flagged') || oldFlags.includes('\\Deleted') || oldFlags.includes('\\Draft')) { - flagsupdate.$set = {}; - if (oldFlags.includes('\\Seen')) { - flagsupdate.$set = { - seen: false - }; - } - if (oldFlags.includes('\\Flagged')) { - flagsupdate.$set = { - flagged: false - }; - } - if (oldFlags.includes('\\Deleted')) { - flagsupdate.$set = { - deleted: false - }; - } - if (oldFlags.includes('\\Draft')) { - flagsupdate.$set = { - draft: false - }; - } + }; + if ( + oldFlags.includes('\\Seen') || + oldFlags.includes('\\Flagged') || + oldFlags.includes('\\Deleted') || + oldFlags.includes('\\Draft') + ) { + flagsupdate.$set = {}; + if (oldFlags.includes('\\Seen')) { + flagsupdate.$set = { + seen: false + }; + } + if (oldFlags.includes('\\Flagged')) { + flagsupdate.$set = { + flagged: false + }; + } + if (oldFlags.includes('\\Deleted')) { + flagsupdate.$set = { + deleted: false + }; + } + if (oldFlags.includes('\\Draft')) { + flagsupdate.$set = { + draft: false + }; } } - break; } + break; + } } if (!update.silent) { // print updated state of the message - session.writeStream.write(session.formatResponse('FETCH', message.uid, { - uid: update.isUid ? message.uid : false, - flags: message.flags - })); + session.writeStream.write( + session.formatResponse('FETCH', message.uid, { + uid: update.isUid ? message.uid : false, + flags: message.flags + }) + ); } if (updated) { @@ -800,11 +910,16 @@ server.onStore = function (path, update, session, callback) { }; // EXPUNGE deletes all messages in selected mailbox marked with \Delete -server.onExpunge = function (path, update, session, callback) { - this.logger.debug({ - tnx: 'expunge', - cid: session.id - }, '[%s] Deleting messages from "%s"', session.id, path); +server.onExpunge = function(path, update, session, callback) { + this.logger.debug( + { + tnx: 'expunge', + cid: session.id + }, + '[%s] Deleting messages from "%s"', + session.id, + path + ); db.database.collection('mailboxes').findOne({ user: session.user.id, path @@ -816,18 +931,20 @@ server.onExpunge = function (path, update, session, callback) { return callback(null, 'NONEXISTENT'); } - let cursor = db.database.collection('messages').find({ - mailbox: mailbox._id, - deleted: true - }).project({ - _id: true, - uid: true, - size: true, - map: true, - magic: true - }).sort([ - ['uid', 1] - ]); + let cursor = db.database + .collection('messages') + .find({ + mailbox: mailbox._id, + deleted: true + }) + .project({ + _id: true, + uid: true, + size: true, + map: true, + magic: true + }) + .sort([['uid', 1]]); let deletedMessages = 0; let deletedStorage = 0; @@ -837,13 +954,17 @@ server.onExpunge = function (path, update, session, callback) { return next(); } - db.database.collection('users').findOneAndUpdate({ - _id: mailbox.user - }, { - $inc: { - storageUsed: -deletedStorage - } - }, next); + db.database.collection('users').findOneAndUpdate( + { + _id: mailbox.user + }, + { + $inc: { + storageUsed: -deletedStorage + } + }, + next + ); }; let processNext = () => { @@ -878,12 +999,17 @@ server.onExpunge = function (path, update, session, callback) { if (!attachments.length) { // not stored attachments - return this.notifier.addEntries(session.user.id, path, { - command: 'EXPUNGE', - ignore: session.id, - uid: message.uid, - message: message._id - }, processNext); + return this.notifier.addEntries( + session.user.id, + path, + { + command: 'EXPUNGE', + ignore: session.id, + uid: message.uid, + message: message._id + }, + processNext + ); } // remove references to attachments (if any exist) @@ -903,12 +1029,17 @@ server.onExpunge = function (path, update, session, callback) { if (err) { // ignore as we don't really care if we have orphans or not } - this.notifier.addEntries(session.user.id, path, { - command: 'EXPUNGE', - ignore: session.id, - uid: message.uid, - message: message._id - }, processNext); + this.notifier.addEntries( + session.user.id, + path, + { + command: 'EXPUNGE', + ignore: session.id, + uid: message.uid, + message: message._id + }, + processNext + ); }); }); }); @@ -919,11 +1050,17 @@ server.onExpunge = function (path, update, session, callback) { }; // COPY / UID COPY sequence mailbox -server.onCopy = function (path, update, session, callback) { - this.logger.debug({ - tnx: 'copy', - cid: session.id - }, '[%s] Copying messages from "%s" to "%s"', session.id, path, update.destination); +server.onCopy = function(path, update, session, callback) { + this.logger.debug( + { + tnx: 'copy', + cid: session.id + }, + '[%s] Copying messages from "%s" to "%s"', + session.id, + path, + update.destination + ); db.database.collection('mailboxes').findOne({ user: session.user.id, path @@ -946,14 +1083,15 @@ server.onCopy = function (path, update, session, callback) { return callback(null, 'TRYCREATE'); } - let cursor = db.database.collection('messages').find({ - mailbox: mailbox._id, - uid: { - $in: update.messages - } - }).sort([ - ['uid', 1] - ]); // no projection as we need to copy the entire message + let cursor = db.database + .collection('messages') + .find({ + mailbox: mailbox._id, + uid: { + $in: update.messages + } + }) + .sort([['uid', 1]]); // no projection as we need to copy the entire message let copiedMessages = 0; let copiedStorage = 0; @@ -962,13 +1100,17 @@ server.onCopy = function (path, update, session, callback) { if (!copiedMessages) { return next(); } - db.database.collection('users').findOneAndUpdate({ - _id: mailbox.user - }, { - $inc: { - storageUsed: copiedStorage - } - }, next); + db.database.collection('users').findOneAndUpdate( + { + _id: mailbox.user + }, + { + $inc: { + storageUsed: copiedStorage + } + }, + next + ); }; let sourceUid = []; @@ -1043,11 +1185,16 @@ server.onCopy = function (path, update, session, callback) { let attachments = Object.keys(message.map || {}).map(key => message.map[key]); if (!attachments.length) { - return this.notifier.addEntries(session.user.id, target.path, { - command: 'EXISTS', - uid: message.uid, - message: message._id - }, processNext); + return this.notifier.addEntries( + session.user.id, + target.path, + { + command: 'EXISTS', + uid: message.uid, + message: message._id + }, + processNext + ); } // update attachments @@ -1067,11 +1214,16 @@ server.onCopy = function (path, update, session, callback) { if (err) { // should we care about this error? } - this.notifier.addEntries(session.user.id, target.path, { - command: 'EXISTS', - uid: message.uid, - message: message._id - }, processNext); + this.notifier.addEntries( + session.user.id, + target.path, + { + command: 'EXISTS', + uid: message.uid, + message: message._id + }, + processNext + ); }); }); }); @@ -1083,44 +1235,58 @@ server.onCopy = function (path, update, session, callback) { }; // MOVE / UID MOVE sequence mailbox -server.onMove = function (path, update, session, callback) { - this.logger.debug({ - tnx: 'move', - cid: session.id - }, '[%s] Moving messages from "%s" to "%s"', session.id, path, update.destination); +server.onMove = function(path, update, session, callback) { + this.logger.debug( + { + tnx: 'move', + cid: session.id + }, + '[%s] Moving messages from "%s" to "%s"', + session.id, + path, + update.destination + ); - messageHandler.move({ - user: session.user.id, - // folder to move messages from - source: { + messageHandler.move( + { user: session.user.id, - path + // folder to move messages from + source: { + user: session.user.id, + path + }, + // folder to move messages to + destination: { + user: session.user.id, + path: update.destination + }, + session, + // list of UIDs to move + messages: update.messages }, - // folder to move messages to - destination: { - user: session.user.id, - path: update.destination - }, - session, - // list of UIDs to move - messages: update.messages - }, (...args) => { - if (args[0]) { - if (args[0].imapResponse) { - return callback(null, args[0].imapResponse); + (...args) => { + if (args[0]) { + if (args[0].imapResponse) { + return callback(null, args[0].imapResponse); + } + return callback(args[0]); } - return callback(args[0]); + callback(...args); } - callback(...args); - }); + ); }; // sends results to socket -server.onFetch = function (path, options, session, callback) { - this.logger.debug({ - tnx: 'fetch', - cid: session.id - }, '[%s] Requested FETCH for "%s"', session.id, path); +server.onFetch = function(path, options, session, callback) { + this.logger.debug( + { + tnx: 'fetch', + cid: session.id + }, + '[%s] Requested FETCH for "%s"', + session.id, + path + ); db.database.collection('mailboxes').findOne({ user: session.user.id, path @@ -1194,12 +1360,7 @@ server.onFetch = function (path, options, session, callback) { return callback(...args); }; - let cursor = db.database.collection('messages'). - find(query). - project(projection). - sort([ - ['uid', 1] - ]); + let cursor = db.database.collection('messages').find(query).project(projection).sort([['uid', 1]]); let rowCount = 0; let processNext = () => { @@ -1223,15 +1384,17 @@ server.onFetch = function (path, options, session, callback) { message.flags.unshift('\\Seen'); } - let stream = imapHandler.compileStream(session.formatResponse('FETCH', message.uid, { - query: options.query, - values: session.getQueryResponse(options.query, message, { - logger: this.logger, - fetchOptions: {}, - database: db.database, - acceptUTF8Enabled: session.isUTF8Enabled() + let stream = imapHandler.compileStream( + session.formatResponse('FETCH', message.uid, { + query: options.query, + values: session.getQueryResponse(options.query, message, { + logger: this.logger, + fetchOptions: {}, + database: db.database, + acceptUTF8Enabled: session.isUTF8Enabled() + }) }) - })); + ); stream.description = util.format('* FETCH #%s uid=%s size=%sB ', ++rowCount, message.uid, message.size); @@ -1247,10 +1410,15 @@ server.onFetch = function (path, options, session, callback) { return processNext(); } - this.logger.debug({ - tnx: 'flags', - cid: session.id - }, '[%s] UPDATE FLAGS for "%s"', session.id, message.uid); + this.logger.debug( + { + tnx: 'flags', + cid: session.id + }, + '[%s] UPDATE FLAGS for "%s"', + session.id, + message.uid + ); isUpdated = true; @@ -1311,7 +1479,7 @@ server.onFetch = function (path, options, session, callback) { * IMAP search can be quite complex, so we optimize here for most common queries to be handled * by MongoDB and then do the final filtering on the client side. This allows */ -server.onSearch = function (path, options, session, callback) { +server.onSearch = function(path, options, session, callback) { db.database.collection('mailboxes').findOne({ user: session.user.id, path @@ -1345,23 +1513,22 @@ server.onSearch = function (path, options, session, callback) { walkQuery(parent, !ne, [].concat(term.value || [])); break; - case 'or': - { - let $or = []; + case 'or': { + let $or = []; - [].concat(term.value || []).forEach(entry => { - walkQuery($or, false, [].concat(entry || [])); + [].concat(term.value || []).forEach(entry => { + walkQuery($or, false, [].concat(entry || [])); + }); + + if ($or.length) { + parent.push({ + $or }); - - if ($or.length) { - parent.push({ - $or - }); - } - - break; } + break; + } + case 'text': // search over entire email case 'body': // search over email body if (term.value && !ne) { @@ -1457,26 +1624,32 @@ server.onSearch = function (path, options, session, callback) { { // FIXME: this does not match unicode symbols for whatever reason let regex = Buffer.from(term.value, 'binary').toString().replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - let entry = term.value ? { - headers: { - $elemMatch: { - key: term.header, - value: !ne ? { - $regex: regex, - $options: 'i' - } : { - $not: { - $regex: regex, - $options: 'i' - } + let entry = term.value + ? { + headers: { + $elemMatch: { + key: term.header, + value: !ne + ? { + $regex: regex, + $options: 'i' + } + : { + $not: { + $regex: regex, + $options: 'i' + } + } } } } - } : { - 'headers.key': !ne ? term.header : { - $ne: term.header - } - }; + : { + 'headers.key': !ne + ? term.header + : { + $ne: term.header + } + }; parent.push(entry); } break; @@ -1499,18 +1672,25 @@ server.onSearch = function (path, options, session, callback) { op = '$gte'; break; } - let entry = !op ? [{ - $gte: value - }, { - $lt: new Date(value.getTime() + 24 * 3600 * 1000) - }] : { - [op]: value - }; + let entry = !op + ? [ + { + $gte: value + }, + { + $lt: new Date(value.getTime() + 24 * 3600 * 1000) + } + ] + : { + [op]: value + }; entry = { - idate: !ne ? entry : { - $not: entry - } + idate: !ne + ? entry + : { + $not: entry + } }; parent.push(entry); @@ -1535,18 +1715,25 @@ server.onSearch = function (path, options, session, callback) { op = '$gte'; break; } - let entry = !op ? [{ - $gte: value - }, { - $lt: new Date(value.getTime() + 24 * 3600 * 1000) - }] : { - [op]: value - }; + let entry = !op + ? [ + { + $gte: value + }, + { + $lt: new Date(value.getTime() + 24 * 3600 * 1000) + } + ] + : { + [op]: value + }; entry = { - hdate: !ne ? entry : { - $not: entry - } + hdate: !ne + ? entry + : { + $not: entry + } }; parent.push(entry); @@ -1577,9 +1764,11 @@ server.onSearch = function (path, options, session, callback) { }; entry = { - size: !ne ? entry : { - $not: entry - } + size: !ne + ? entry + : { + $not: entry + } }; parent.push(entry); @@ -1595,14 +1784,17 @@ server.onSearch = function (path, options, session, callback) { query.$and = $and; } - this.logger.info({ - tnx: 'search', - cid: session.id - }, '[%s] SEARCH %s', session.id, JSON.stringify(query)); + this.logger.info( + { + tnx: 'search', + cid: session.id + }, + '[%s] SEARCH %s', + session.id, + JSON.stringify(query) + ); - let cursor = db.database.collection('messages'). - find(query). - project({ + let cursor = db.database.collection('messages').find(query).project({ uid: true, modseq: true }); @@ -1613,17 +1805,25 @@ server.onSearch = function (path, options, session, callback) { let processNext = () => { cursor.next((err, message) => { if (err) { - this.logger.error({ - tnx: 'search', - cid: session.id - }, '[%s] SEARCHFAIL %s error="%s"', session.id, JSON.stringify(query), err.message); + this.logger.error( + { + tnx: 'search', + cid: session.id + }, + '[%s] SEARCHFAIL %s error="%s"', + session.id, + JSON.stringify(query), + err.message + ); return callback(new Error('Can not make requested search query')); } if (!message) { - return cursor.close(() => callback(null, { - uidList, - highestModseq - })); + return cursor.close(() => + callback(null, { + uidList, + highestModseq + }) + ); } if (highestModseq < message.modseq) { @@ -1639,11 +1839,16 @@ server.onSearch = function (path, options, session, callback) { }); }; -server.onGetQuotaRoot = function (path, session, callback) { - this.logger.debug({ - tnx: 'quota', - cid: session.id - }, '[%s] Requested quota root info for "%s"', session.id, path); +server.onGetQuotaRoot = function(path, session, callback) { + this.logger.debug( + { + tnx: 'quota', + cid: session.id + }, + '[%s] Requested quota root info for "%s"', + session.id, + path + ); db.database.collection('mailboxes').findOne({ user: session.user.id, @@ -1675,11 +1880,16 @@ server.onGetQuotaRoot = function (path, session, callback) { }); }; -server.onGetQuota = function (quotaRoot, session, callback) { - this.logger.debug({ - tnx: 'quota', - cid: session.id - }, '[%s] Requested quota info for "%s"', session.id, quotaRoot); +server.onGetQuota = function(quotaRoot, session, callback) { + this.logger.debug( + { + tnx: 'quota', + cid: session.id + }, + '[%s] Requested quota info for "%s"', + session.id, + quotaRoot + ); if (quotaRoot !== '') { return callback(null, 'NONEXISTENT'); @@ -1760,12 +1970,16 @@ function clearExpiredMessages() { clearTimeout(gcTimeout); // First, acquire the lock. This prevents multiple connected clients for deleting the same messages - gcLock.acquireLock('gc_expired', 10 * 60 * 1000 /* Lock expires after 10min if not released */ , (err, lock) => { + gcLock.acquireLock('gc_expired', 10 * 60 * 1000 /* Lock expires after 10min if not released */, (err, lock) => { if (err) { - server.logger.error({ - tnx: 'gc', - err - }, 'Failed to acquire lock error=%s', err.message); + server.logger.error( + { + tnx: 'gc', + err + }, + 'Failed to acquire lock error=%s', + err.message + ); gcTimeout = setTimeout(clearExpiredMessages, GC_INTERVAL); gcTimeout.unref(); return; @@ -1778,29 +1992,36 @@ function clearExpiredMessages() { let done = () => { gcLock.releaseLock(lock, err => { if (err) { - server.logger.error({ - tnx: 'gc', - err - }, 'Failed to release lock error=%s', err.message); + server.logger.error( + { + tnx: 'gc', + err + }, + 'Failed to release lock error=%s', + err.message + ); } gcTimeout = setTimeout(clearExpiredMessages, GC_INTERVAL); gcTimeout.unref(); }); }; - let cursor = db.database.collection('messages').find({ - exp: true, - rdate: { - $lte: Date.now() - } - }).project({ - _id: true, - mailbox: true, - uid: true, - size: true, - map: true, - magic: true - }); + let cursor = db.database + .collection('messages') + .find({ + exp: true, + rdate: { + $lte: Date.now() + } + }) + .project({ + _id: true, + mailbox: true, + uid: true, + size: true, + map: true, + magic: true + }); let deleted = 0; let processNext = () => { @@ -1812,36 +2033,46 @@ function clearExpiredMessages() { return cursor.close(() => { // delete all attachments that do not have any active links to message objects deleteOrphanedAttachments(() => { - server.logger.debug({ - tnx: 'gc' - }, 'Deleted %s messages', deleted); + server.logger.debug( + { + tnx: 'gc' + }, + 'Deleted %s messages', + deleted + ); done(null, true); }); }); } - server.logger.info({ - tnx: 'gc', - err - }, 'Deleting expired message id=%s', message._id); + server.logger.info( + { + tnx: 'gc', + err + }, + 'Deleting expired message id=%s', + message._id + ); gcTimeout = setTimeout(clearExpiredMessages, GC_INTERVAL); - messageHandler.del({ - message, - skipAttachments: true - }, err => { - if (err) { - return cursor.close(() => done(err)); + messageHandler.del( + { + message, + skipAttachments: true + }, + err => { + if (err) { + return cursor.close(() => done(err)); + } + deleted++; + processNext(); } - deleted++; - processNext(); - }); + ); }); }; processNext(); - }); } @@ -1851,7 +2082,6 @@ module.exports = done => { } let start = () => { - messageHandler = new MessageHandler(db.database, db.redisConfig); userHandler = new UserHandler(db.database, db.redis); @@ -1871,9 +2101,12 @@ module.exports = done => { started = true; return done(err); } - server.logger.error({ + server.logger.error( + { + err + }, err - }, err); + ); }); // start listening @@ -1889,9 +2122,13 @@ module.exports = done => { let indexpos = 0; let ensureIndexes = () => { if (indexpos >= setupIndexes.length) { - server.logger.info({ - tnx: 'mongo' - }, 'Setup indexes for %s collections', setupIndexes.length); + server.logger.info( + { + tnx: 'mongo' + }, + 'Setup indexes for %s collections', + setupIndexes.length + ); gcLock = new RedFour({ redis: db.redisConfig, diff --git a/indexes.json b/indexes.json index 626e82af..e6f44b69 100644 --- a/indexes.json +++ b/indexes.json @@ -154,7 +154,6 @@ } }, { "name": "retention_time", - "expireAfterSeconds": 0, "key": { "exp": 1, "rdate": 1 @@ -184,7 +183,7 @@ } }, { "name": "autoexpire", - "expireAfterSeconds": "21600", + "expireAfterSeconds": 21600, "key": { "created": 1 } diff --git a/lib/message-handler.js b/lib/message-handler.js index d4ddf8ce..d3ad6c94 100644 --- a/lib/message-handler.js +++ b/lib/message-handler.js @@ -31,7 +31,6 @@ const IGNORE_HEADERS = [ ]; class MessageHandler { - constructor(database, redisConfig) { this.database = database; this.redis = redisConfig || tools.redisConfig(config.redis); @@ -79,7 +78,6 @@ class MessageHandler { // Monster method for inserting new messages to a mailbox // TODO: Refactor into smaller pieces add(options, callback) { - let prepared = options.prepared || this.prepareMessage(options); let id = prepared.id; @@ -100,208 +98,212 @@ class MessageHandler { return callback(err); } - this.checkExistingMessage(mailbox._id, { - hdate, - msgid, - flags - }, options, (...args) => { - if (args[0] || args[1]) { - return callback(...args); - } - - let cleanup = (...args) => { - - if (!args[0]) { + this.checkExistingMessage( + mailbox._id, + { + hdate, + msgid, + flags + }, + options, + (...args) => { + if (args[0] || args[1]) { return callback(...args); } - let attachments = Object.keys(maildata.map || {}).map(key => maildata.map[key]); - if (!attachments.length) { - return callback(...args); - } - - // error occured, remove attachments - this.database.collection('attachments.files').deleteMany({ - _id: { - $in: attachments - } - }, err => { - if (err) { - // ignore as we don't really care if we have orphans or not + let cleanup = (...args) => { + if (!args[0]) { + return callback(...args); } - return callback(null, true); - }); - }; + let attachments = Object.keys(maildata.map || {}).map(key => maildata.map[key]); + if (!attachments.length) { + return callback(...args); + } - this.indexer.storeNodeBodies(id, maildata, mimeTree, err => { - if (err) { - return cleanup(err); - } - - // prepare message object - let message = { - _id: id, - - v: SCHEMA_VERSION, - - // if true then expirest after rdate + retention - exp: !!mailbox.retention, - rdate: Date.now() + (mailbox.retention || 0), - - idate, - hdate, - flags, - size, - - // some custom metadata about the delivery - meta: options.meta || {}, - - // list filter IDs that matched this message - filters: Array.isArray(options.filters) ? options.filters : [].concat(options.filters || []), - - headers, - mimeTree, - envelope, - bodystructure, - msgid, - - // use boolean for more commonly used (and searched for) flags - seen: flags.includes('\\Seen'), - flagged: flags.includes('\\Flagged'), - deleted: flags.includes('\\Deleted'), - draft: flags.includes('\\Draft'), - - magic: maildata.magic, - map: maildata.map + // error occured, remove attachments + this.database.collection('attachments.files').deleteMany({ + _id: { + $in: attachments + } + }, () => callback(...args)); }; - if (maildata.attachments && maildata.attachments.length) { - message.attachments = maildata.attachments; - message.ha = true; - } else { - message.ha = false; - } - - let maxTextLength = 300 * 1024; - - if (maildata.text) { - message.text = maildata.text.replace(/\r\n/g, '\n').trim(); - message.text = message.text.length <= maxTextLength ? message.text : message.text.substr(0, maxTextLength); - message.intro = message.text.replace(/\s+/g, ' ').trim(); - if (message.intro.length > 128) { - let intro = message.intro.substr(0, 128); - let lastSp = intro.lastIndexOf(' '); - if (lastSp > 0) { - intro = intro.substr(0, lastSp); - } - message.intro = intro + '…'; - } - } - - if (maildata.html && maildata.html.length) { - let htmlSize = 0; - message.html = maildata.html.map(html => { - if (htmlSize >= maxTextLength || !html) { - return ''; - } - - if (htmlSize + Buffer.byteLength(html) <= maxTextLength) { - htmlSize += Buffer.byteLength(html); - return html; - } - - html = html.substr(0, htmlSize + Buffer.byteLength(html) - maxTextLength); - htmlSize += Buffer.byteLength(html); - return html; - }).filter(html => html); - } - - - this.database.collection('users').findOneAndUpdate({ - _id: mailbox.user - }, { - $inc: { - storageUsed: size - } - }, err => { + this.indexer.storeNodeBodies(id, maildata, mimeTree, err => { if (err) { return cleanup(err); } - let rollback = err => { - this.database.collection('users').findOneAndUpdate({ - _id: mailbox.user - }, { - $inc: { - storageUsed: -size - } - }, () => { - cleanup(err); - }); + // prepare message object + let message = { + _id: id, + + v: SCHEMA_VERSION, + + // if true then expirest after rdate + retention + exp: !!mailbox.retention, + rdate: Date.now() + (mailbox.retention || 0), + + idate, + hdate, + flags, + size, + + // some custom metadata about the delivery + meta: options.meta || {}, + + // list filter IDs that matched this message + filters: Array.isArray(options.filters) ? options.filters : [].concat(options.filters || []), + + headers, + mimeTree, + envelope, + bodystructure, + msgid, + + // use boolean for more commonly used (and searched for) flags + seen: flags.includes('\\Seen'), + flagged: flags.includes('\\Flagged'), + deleted: flags.includes('\\Deleted'), + draft: flags.includes('\\Draft'), + + magic: maildata.magic, + map: maildata.map }; - // acquire new UID+MODSEQ - this.database.collection('mailboxes').findOneAndUpdate({ - _id: mailbox._id + if (maildata.attachments && maildata.attachments.length) { + message.attachments = maildata.attachments; + message.ha = true; + } else { + message.ha = false; + } + + let maxTextLength = 300 * 1024; + + if (maildata.text) { + message.text = maildata.text.replace(/\r\n/g, '\n').trim(); + message.text = message.text.length <= maxTextLength ? message.text : message.text.substr(0, maxTextLength); + message.intro = message.text.replace(/\s+/g, ' ').trim(); + if (message.intro.length > 128) { + let intro = message.intro.substr(0, 128); + let lastSp = intro.lastIndexOf(' '); + if (lastSp > 0) { + intro = intro.substr(0, lastSp); + } + message.intro = intro + '…'; + } + } + + if (maildata.html && maildata.html.length) { + let htmlSize = 0; + message.html = maildata.html + .map(html => { + if (htmlSize >= maxTextLength || !html) { + return ''; + } + + if (htmlSize + Buffer.byteLength(html) <= maxTextLength) { + htmlSize += Buffer.byteLength(html); + return html; + } + + html = html.substr(0, htmlSize + Buffer.byteLength(html) - maxTextLength); + htmlSize += Buffer.byteLength(html); + return html; + }) + .filter(html => html); + } + + this.database.collection('users').findOneAndUpdate({ + _id: mailbox.user }, { $inc: { - // allocate bot UID and MODSEQ values so when journal is later sorted by - // modseq then UIDs are always in ascending order - uidNext: 1, - modifyIndex: 1 + storageUsed: size } - }, (err, item) => { + }, err => { if (err) { - return rollback(err); + return cleanup(err); } - if (!item || !item.value) { - // was not able to acquire a lock - let err = new Error('Mailbox is missing'); - err.imapResponse = 'TRYCREATE'; - return rollback(err); - } + let rollback = err => { + this.database.collection('users').findOneAndUpdate({ + _id: mailbox.user + }, { + $inc: { + storageUsed: -size + } + }, () => { + cleanup(err); + }); + }; - let mailbox = item.value; - - // updated message object by setting mailbox specific values - message.mailbox = mailbox._id; - message.user = mailbox.user; - message.uid = mailbox.uidNext; - message.modseq = mailbox.modifyIndex + 1; - - this.database.collection('messages').insertOne(message, err => { + // acquire new UID+MODSEQ + this.database.collection('mailboxes').findOneAndUpdate({ + _id: mailbox._id + }, { + $inc: { + // allocate bot UID and MODSEQ values so when journal is later sorted by + // modseq then UIDs are always in ascending order + uidNext: 1, + modifyIndex: 1 + } + }, (err, item) => { if (err) { return rollback(err); } - let uidValidity = mailbox.uidValidity; - let uid = message.uid; - - if (options.session && options.session.selected && options.session.selected.mailbox === mailbox.path) { - options.session.writeStream.write(options.session.formatResponse('EXISTS', message.uid)); + if (!item || !item.value) { + // was not able to acquire a lock + let err = new Error('Mailbox is missing'); + err.imapResponse = 'TRYCREATE'; + return rollback(err); } - this.notifier.addEntries(mailbox, false, { - command: 'EXISTS', - uid: message.uid, - ignore: options.session && options.session.id, - message: message._id, - modseq: message.modseq - }, () => { - this.notifier.fire(mailbox.user, mailbox.path); - return cleanup(null, true, { - uidValidity, - uid, - id: message._id - }); + let mailbox = item.value; + + // updated message object by setting mailbox specific values + message.mailbox = mailbox._id; + message.user = mailbox.user; + message.uid = mailbox.uidNext; + message.modseq = mailbox.modifyIndex + 1; + + this.database.collection('messages').insertOne(message, err => { + if (err) { + return rollback(err); + } + + let uidValidity = mailbox.uidValidity; + let uid = message.uid; + + if (options.session && options.session.selected && options.session.selected.mailbox === mailbox.path) { + options.session.writeStream.write(options.session.formatResponse('EXISTS', message.uid)); + } + + this.notifier.addEntries( + mailbox, + false, + { + command: 'EXISTS', + uid: message.uid, + ignore: options.session && options.session.id, + message: message._id, + modseq: message.modseq + }, + () => { + this.notifier.fire(mailbox.user, mailbox.path); + return cleanup(null, true, { + uidValidity, + uid, + id: message._id + }); + } + ); }); }); }); }); - }); - }); + } + ); }); } @@ -387,28 +389,37 @@ class MessageHandler { if (options.session && options.session.selected && options.session.selected.mailbox === mailbox.path) { options.session.writeStream.write(options.session.formatResponse('EXISTS', updated.uid)); } - this.notifier.addEntries(mailbox, false, { - command: 'EXPUNGE', - ignore: options.session && options.session.id, - uid: existing.uid, - message: existing._id - }, () => { - - this.notifier.addEntries(mailbox, false, { - command: 'EXISTS', - uid: updated.uid, + this.notifier.addEntries( + mailbox, + false, + { + command: 'EXPUNGE', ignore: options.session && options.session.id, - message: updated._id, - modseq: updated.modseq - }, () => { - this.notifier.fire(mailbox.user, mailbox.path); - return callback(null, true, { - uidValidity: mailbox.uidValidity, - uid, - id: existing._id - }); - }); - }); + uid: existing.uid, + message: existing._id + }, + () => { + this.notifier.addEntries( + mailbox, + false, + { + command: 'EXISTS', + uid: updated.uid, + ignore: options.session && options.session.id, + message: updated._id, + modseq: updated.modseq + }, + () => { + this.notifier.fire(mailbox.user, mailbox.path); + return callback(null, true, { + uidValidity: mailbox.uidValidity, + uid, + id: existing._id + }); + } + ); + } + ); }); }); }); @@ -417,30 +428,37 @@ class MessageHandler { updateQuota(mailbox, inc, callback) { inc = inc || {}; - this.database.collection('users').findOneAndUpdate({ - _id: mailbox.user - }, { - $inc: { - storageUsed: Number(inc.storageUsed) || 0 - } - }, callback); + this.database.collection('users').findOneAndUpdate( + { + _id: mailbox.user + }, + { + $inc: { + storageUsed: Number(inc.storageUsed) || 0 + } + }, + callback + ); } del(options, callback) { - let getMessage = next => { if (options.message) { return next(null, options.message); } - this.database.collection('messages').findOne(options.query, { - fields: { - mailbox: true, - uid: true, - size: true, - map: true, - magic: true - } - }, next); + this.database.collection('messages').findOne( + options.query, + { + fields: { + mailbox: true, + uid: true, + size: true, + map: true, + magic: true + } + }, + next + ); }; getMessage((err, message) => { @@ -452,75 +470,85 @@ class MessageHandler { return callback(new Error('Message does not exist')); } - this.getMailbox({ - mailbox: options.mailbox || message.mailbox - }, (err, mailbox) => { - if (err) { - return callback(err); - } - - this.database.collection('messages').deleteOne({ - _id: message._id - }, err => { + this.getMailbox( + { + mailbox: options.mailbox || message.mailbox + }, + (err, mailbox) => { if (err) { return callback(err); } - this.updateQuota(mailbox, { - storageUsed: -message.size - }, () => { + this.database.collection('messages').deleteOne({ + _id: message._id + }, err => { + if (err) { + return callback(err); + } - let updateAttachments = next => { - let attachments = Object.keys(message.map || {}).map(key => message.map[key]); - if (!attachments.length) { - return next(); + this.updateQuota( + mailbox, + { + storageUsed: -message.size + }, + () => { + let updateAttachments = next => { + let attachments = Object.keys(message.map || {}).map(key => message.map[key]); + if (!attachments.length) { + return next(); + } + + // remove link to message from attachments (if any exist) + this.database.collection('attachments.files').updateMany({ + _id: { + $in: attachments + } + }, { + $inc: { + 'metadata.c': -1, + 'metadata.m': -message.magic + } + }, { + multi: true, + w: 1 + }, err => { + if (err) { + // ignore as we don't really care if we have orphans or not + } + next(); + }); + }; + + updateAttachments(() => { + if (options.session && options.session.selected && options.session.selected.mailbox === mailbox.path) { + options.session.writeStream.write(options.session.formatResponse('EXPUNGE', message.uid)); + } + + this.notifier.addEntries( + mailbox, + false, + { + command: 'EXPUNGE', + ignore: options.session && options.session.id, + uid: message.uid, + message: message._id + }, + () => { + this.notifier.fire(mailbox.user, mailbox.path); + + if (options.skipAttachments) { + return callback(null, true); + } + + return callback(null, true); + } + ); + }); } - - // remove link to message from attachments (if any exist) - this.database.collection('attachments.files').updateMany({ - _id: { - $in: attachments - } - }, { - $inc: { - 'metadata.c': -1, - 'metadata.m': -message.magic - } - }, { - multi: true, - w: 1 - }, err => { - if (err) { - // ignore as we don't really care if we have orphans or not - } - next(); - }); - }; - - updateAttachments(() => { - - if (options.session && options.session.selected && options.session.selected.mailbox === mailbox.path) { - options.session.writeStream.write(options.session.formatResponse('EXPUNGE', message.uid)); - } - - this.notifier.addEntries(mailbox, false, { - command: 'EXPUNGE', - ignore: options.session && options.session.id, - uid: message.uid, - message: message._id - }, () => { - this.notifier.fire(mailbox.user, mailbox.path); - - if (options.skipAttachments) { - return callback(null, true); - } - - return callback(null, true); - }); - }); + ); }); - }); - }); + } + ); }); } @@ -546,17 +574,18 @@ class MessageHandler { }, { uidNext: true }, () => { - - let cursor = this.database.collection('messages').find({ - mailbox: mailbox._id, - uid: { - $in: options.messages || [] - } - }).project({ - uid: 1 - }).sort([ - ['uid', 1] - ]); + let cursor = this.database + .collection('messages') + .find({ + mailbox: mailbox._id, + uid: { + $in: options.messages || [] + } + }) + .project({ + uid: 1 + }) + .sort([['uid', 1]]); let sourceUid = []; let destinationUid = []; @@ -565,7 +594,6 @@ class MessageHandler { let existsEntries = []; let done = err => { - let next = () => { if (err) { return callback(err); @@ -692,43 +720,45 @@ class MessageHandler { } generateIndexedHeaders(headersArray) { - return (headersArray || []).map(line => { - line = Buffer.from(line, 'binary').toString(); + return (headersArray || []) + .map(line => { + line = Buffer.from(line, 'binary').toString(); - let key = line.substr(0, line.indexOf(':')).trim().toLowerCase(); + let key = line.substr(0, line.indexOf(':')).trim().toLowerCase(); - if (IGNORE_HEADERS.find(prefix => key.indexOf(prefix) === 0)) { - // do not index this header - return false; - } + if (IGNORE_HEADERS.find(prefix => key.indexOf(prefix) === 0)) { + // do not index this header + return false; + } - let value = line.substr(line.indexOf(':') + 1).trim().toLowerCase().replace(/\s*\r?\n\s*/g, ' '); + let value = line.substr(line.indexOf(':') + 1).trim().toLowerCase().replace(/\s*\r?\n\s*/g, ' '); - try { - value = libmime.decodeWords(value); - } catch (E) { - // ignore - } + try { + value = libmime.decodeWords(value); + } catch (E) { + // ignore + } - // trim long values as mongodb indexed fields can not be too long + // trim long values as mongodb indexed fields can not be too long - if (Buffer.byteLength(key, 'utf-8') >= 255) { - key = Buffer.from(key).slice(0, 255).toString(); - key = key.substr(0, key.length - 4); - } + if (Buffer.byteLength(key, 'utf-8') >= 255) { + key = Buffer.from(key).slice(0, 255).toString(); + key = key.substr(0, key.length - 4); + } - if (Buffer.byteLength(value, 'utf-8') >= 880) { - // value exceeds MongoDB max indexed value length - value = Buffer.from(value).slice(0, 880).toString(); - // remove last 4 chars to be sure we do not have any incomplete unicode sequences - value = value.substr(0, value.length - 4); - } + if (Buffer.byteLength(value, 'utf-8') >= 880) { + // value exceeds MongoDB max indexed value length + value = Buffer.from(value).slice(0, 880).toString(); + // remove last 4 chars to be sure we do not have any incomplete unicode sequences + value = value.substr(0, value.length - 4); + } - return { - key, - value - }; - }).filter(line => line); + return { + key, + value + }; + }) + .filter(line => line); } prepareMessage(options) { @@ -740,8 +770,8 @@ class MessageHandler { let bodystructure = this.indexer.getBodyStructure(mimeTree); let envelope = this.indexer.getEnvelope(mimeTree); - let idate = options.date && new Date(options.date) || new Date(); - let hdate = mimeTree.parsedHeader.date && new Date(mimeTree.parsedHeader.date) || false; + let idate = (options.date && new Date(options.date)) || new Date(); + let hdate = (mimeTree.parsedHeader.date && new Date(mimeTree.parsedHeader.date)) || false; let flags = [].concat(options.flags || []); @@ -749,7 +779,7 @@ class MessageHandler { hdate = idate; } - let msgid = envelope[9] || ('<' + uuidV1() + '@wildduck.email>'); + let msgid = envelope[9] || '<' + uuidV1() + '@wildduck.email>'; let headers = this.generateIndexedHeaders(mimeTree.header);