mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-01-01 13:13:53 +08:00
Do not explode on messages that have a rfc822 message as an attachment
This commit is contained in:
parent
6773863038
commit
9534594a7e
7 changed files with 1250 additions and 980 deletions
|
@ -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
|
||||
|
|
|
@ -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 || '';
|
||||
|
|
|
@ -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 = '<p>' + 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, '</p><p>').trim(). // insert <p> to multiple linebreaks
|
||||
replace(/\n/g, '<br/>') + // insert <br> to single linebreaks
|
||||
let text =
|
||||
'<p>' +
|
||||
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, '</p><p>')
|
||||
.trim() // insert <p> to multiple linebreaks
|
||||
.replace(/\n/g, '<br/>') + // insert <br> to single linebreaks
|
||||
'</p>';
|
||||
|
||||
return text;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue