mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-11-15 05:44:36 +08:00
605 lines
16 KiB
JavaScript
605 lines
16 KiB
JavaScript
'use strict';
|
||
|
||
let Indexer = require('./indexer/indexer');
|
||
let utf7 = require('utf7').imap;
|
||
|
||
module.exports.systemFlags = ['\\answered', '\\flagged', '\\draft', '\\deleted', '\\seen'];
|
||
|
||
module.exports.fetchSchema = {
|
||
body: [true, {
|
||
type: /^(\d+\.)*(CONTENT|HEADER|HEADER\.FIELDS|HEADER\.FIELDS\.NOT|TEXT|MIME|\d+)$/i,
|
||
headers: /^(\d+\.)*(HEADER\.FIELDS|HEADER\.FIELDS\.NOT)$/i,
|
||
startFrom: 'optional',
|
||
maxLength: 'optional'
|
||
}],
|
||
bodystructure: true,
|
||
envelope: true,
|
||
flags: true,
|
||
internaldate: true,
|
||
rfc822: true,
|
||
'rfc822.header': true,
|
||
'rfc822.size': true,
|
||
'rfc822.text': true,
|
||
modseq: true,
|
||
uid: true
|
||
};
|
||
|
||
module.exports.searchSchema = {
|
||
charset: ['string'],
|
||
all: true,
|
||
answered: true,
|
||
bcc: ['string'],
|
||
before: ['date'],
|
||
body: ['string'],
|
||
cc: ['string'],
|
||
deleted: true,
|
||
draft: true,
|
||
flagged: true,
|
||
from: ['string'],
|
||
header: ['string', 'string'],
|
||
keyword: ['string'],
|
||
larger: ['number'],
|
||
modseq: [
|
||
['string', 'string', 'number'],
|
||
['number']
|
||
],
|
||
new: true,
|
||
not: ['expression'],
|
||
old: true,
|
||
on: ['date'],
|
||
or: ['expression', 'expression'],
|
||
recent: true,
|
||
seen: true,
|
||
sentbefore: ['date'],
|
||
senton: ['date'],
|
||
sentsince: ['date'],
|
||
since: ['date'],
|
||
smaller: ['number'],
|
||
subject: ['string'],
|
||
text: ['string'],
|
||
to: ['string'],
|
||
uid: ['sequence'],
|
||
unanswered: true,
|
||
undeleted: true,
|
||
undraft: true,
|
||
unflagged: true,
|
||
unkeyword: ['string'],
|
||
unseen: true
|
||
};
|
||
|
||
module.exports.searchMapping = {
|
||
all: {
|
||
key: 'all',
|
||
value: [true]
|
||
},
|
||
answered: {
|
||
key: 'flag',
|
||
value: ['\\Answered', true]
|
||
},
|
||
bcc: {
|
||
key: 'header',
|
||
value: ['bcc', '$1']
|
||
},
|
||
before: {
|
||
key: 'internaldate',
|
||
value: ['<', '$1']
|
||
},
|
||
cc: {
|
||
key: 'header',
|
||
value: ['cc', '$1']
|
||
},
|
||
deleted: {
|
||
key: 'flag',
|
||
value: ['\\Deleted', true]
|
||
},
|
||
draft: {
|
||
key: 'flag',
|
||
value: ['\\Draft', true]
|
||
},
|
||
flagged: {
|
||
key: 'flag',
|
||
value: ['\\Flagged', true]
|
||
},
|
||
from: {
|
||
key: 'header',
|
||
value: ['from', '$1']
|
||
},
|
||
keyword: {
|
||
key: 'flag',
|
||
value: ['$1', true]
|
||
},
|
||
larger: {
|
||
key: 'size',
|
||
value: ['>', '$1']
|
||
},
|
||
new: {
|
||
key: 'flag',
|
||
value: ['\\Recent', true, '\\Seen', false]
|
||
},
|
||
old: {
|
||
key: 'flag',
|
||
value: ['\\Recent', false]
|
||
},
|
||
on: {
|
||
key: 'internaldate',
|
||
value: ['=', '$1']
|
||
},
|
||
recent: {
|
||
key: 'flag',
|
||
value: ['\\Recent', true]
|
||
},
|
||
seen: {
|
||
key: 'flag',
|
||
value: ['\\Seen', true]
|
||
},
|
||
sentbefore: {
|
||
key: 'date',
|
||
value: ['<', '$1']
|
||
},
|
||
senton: {
|
||
key: 'date',
|
||
value: ['=', '$1']
|
||
},
|
||
sentsince: {
|
||
key: 'date',
|
||
value: ['>=', '$1']
|
||
},
|
||
since: {
|
||
key: 'internaldate',
|
||
value: ['>=', '$1']
|
||
},
|
||
smaller: {
|
||
key: 'size',
|
||
value: ['<', '$1']
|
||
},
|
||
subject: {
|
||
key: 'header',
|
||
value: ['subject', '$1']
|
||
},
|
||
to: {
|
||
key: 'header',
|
||
value: ['to', '$1']
|
||
},
|
||
unanswered: {
|
||
key: 'flag',
|
||
value: ['\\Answered', false]
|
||
},
|
||
undeleted: {
|
||
key: 'flag',
|
||
value: ['\\Deleted', false]
|
||
},
|
||
undraft: {
|
||
key: 'flag',
|
||
value: ['\\Draft', false]
|
||
},
|
||
unflagged: {
|
||
key: 'flag',
|
||
value: ['\\Flagged', false]
|
||
},
|
||
unkeyword: {
|
||
key: 'flag',
|
||
value: ['$1', false]
|
||
},
|
||
unseen: {
|
||
key: 'flag',
|
||
value: ['\\Seen', false]
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Checks if a sequence range string is valid or not
|
||
*
|
||
* @param {range} range Sequence range, eg "1,2,3:7"
|
||
* @returns {Boolean} True if the string looks like a sequence range
|
||
*/
|
||
module.exports.validateSequnce = function (range) {
|
||
return !!(range.length && /^(\d+|\*)(:\d+|:\*)?(,(\d+|\*)(:\d+|:\*)?)*$/.test(range));
|
||
};
|
||
|
||
module.exports.normalizeMailbox = function (mailbox, utf7Encoded) {
|
||
if (!mailbox) {
|
||
return '';
|
||
}
|
||
|
||
// trim slashes
|
||
mailbox = mailbox.replace(/^\/|\/$/g, () => '');
|
||
|
||
// Normalize case insensitive INBOX to always use uppercase
|
||
let parts = mailbox.split('/');
|
||
if (parts[0].toUpperCase() === 'INBOX') {
|
||
parts[0] = 'INBOX';
|
||
}
|
||
|
||
if (utf7Encoded) {
|
||
parts = parts.map(value => utf7.decode(value));
|
||
}
|
||
|
||
mailbox = parts.join('/');
|
||
|
||
return mailbox;
|
||
};
|
||
|
||
module.exports.generateFolderListing = function (folders, skipHierarchy) {
|
||
let items = new Map();
|
||
let parents = [];
|
||
|
||
folders.forEach(folder => {
|
||
let item;
|
||
|
||
if (typeof folder === 'string') {
|
||
folder = {
|
||
path: folder
|
||
};
|
||
}
|
||
|
||
if (!folder || typeof folder !== 'object') {
|
||
return;
|
||
}
|
||
|
||
let path = module.exports.normalizeMailbox(folder.path);
|
||
let parent, parentPath;
|
||
if (!path) {
|
||
return;
|
||
}
|
||
parent = path.split('/');
|
||
parent.pop();
|
||
|
||
while (parent.length) {
|
||
parentPath = parent.join('/');
|
||
if (parent && parents.indexOf(parentPath) < 0) {
|
||
parents.push(parentPath);
|
||
}
|
||
parent.pop();
|
||
}
|
||
|
||
item = {
|
||
flags: [].concat(folder.flags || []),
|
||
path
|
||
};
|
||
|
||
if (typeof folder.specialUse === 'string' && folder.specialUse) {
|
||
item.specialUse = folder.specialUse;
|
||
}
|
||
|
||
items.set(path, item);
|
||
});
|
||
|
||
// ensure INBOX
|
||
if (!items.has('INBOX')) {
|
||
items.set('INBOX', {
|
||
path: 'INBOX',
|
||
flags: []
|
||
});
|
||
}
|
||
|
||
// Adds \HasChildren flag for parent folders
|
||
parents.forEach(path => {
|
||
if (!items.has(path) && !skipHierarchy) {
|
||
// add virtual hierarchy folders
|
||
items.set(path, {
|
||
flags: ['\\Noselect'],
|
||
path
|
||
});
|
||
}
|
||
let parent = items.get(path);
|
||
|
||
if (parent && parent.flags.indexOf('\\HasChildren') < 0) {
|
||
parent.flags.push('\\HasChildren');
|
||
}
|
||
});
|
||
|
||
// converts cache Map to a response array
|
||
let result = [];
|
||
items.forEach(folder => {
|
||
// Adds \HasNoChildren flag for leaf folders
|
||
if (folder.flags.indexOf('\\HasChildren') < 0 && folder.flags.indexOf('\\HasNoChildren') < 0) {
|
||
folder.flags.push('\\HasNoChildren');
|
||
}
|
||
result.push(folder);
|
||
});
|
||
|
||
// sorts folders
|
||
result.sort((a, b) => {
|
||
let aParts = a.path.split('/');
|
||
let bParts = b.path.split('/');
|
||
for (let i = 0; i < aParts.length; i++) {
|
||
if (!bParts[i]) {
|
||
return 1;
|
||
}
|
||
if (aParts[i] !== bParts[i]) {
|
||
// prefer INBOX when sorting
|
||
if (i === 0 && aParts[i] === 'INBOX') {
|
||
return -1;
|
||
} else if (i === 0 && bParts[i] === 'INBOX') {
|
||
return 1;
|
||
}
|
||
return aParts[i].localeCompare(bParts[i]);
|
||
}
|
||
}
|
||
return 0;
|
||
});
|
||
|
||
return result;
|
||
};
|
||
|
||
module.exports.filterFolders = function (folders, query) {
|
||
query = query
|
||
// remove excess * and %
|
||
.replace(/\*\*+/g, '*').replace(/%%+/g, '%')
|
||
// escape special characters
|
||
.replace(/([\\^$+?!.():=\[\]|,\-])/g, '\\$1')
|
||
// setup *
|
||
.replace(/[*]/g, '.*')
|
||
// setup %
|
||
.replace(/[%]/g, '[^\/]*');
|
||
|
||
let regex = new RegExp('^' + query + '$', '');
|
||
|
||
return folders.filter(folder => !!regex.test(folder.path));
|
||
};
|
||
|
||
module.exports.getMessageRange = function (uidList, range, isUid) {
|
||
range = (range || '').toString();
|
||
|
||
let result = [];
|
||
let rangeParts = range.split(',');
|
||
let uid, i, len;
|
||
let totalMessages = uidList.length;
|
||
let maxUid = 0;
|
||
|
||
let inRange = (nr, ranges, total) => {
|
||
let range, from, to;
|
||
for (let i = 0, len = ranges.length; i < len; i++) {
|
||
range = ranges[i];
|
||
to = range.split(':');
|
||
from = to.shift();
|
||
if (from === '*') {
|
||
from = total;
|
||
}
|
||
from = Number(from) || 1;
|
||
to = to.pop() || from;
|
||
to = Number(to === '*' && total || to) || from;
|
||
|
||
if (nr >= Math.min(from, to) && nr <= Math.max(from, to)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
|
||
for (i = 0, len = uidList.length; i < len; i++) {
|
||
if (uidList[i] > maxUid) {
|
||
maxUid = uidList[i];
|
||
}
|
||
}
|
||
|
||
for (i = 0, len = uidList.length; i < len; i++) {
|
||
uid = uidList[i] || 1;
|
||
if (inRange(isUid ? uid : i + 1, rangeParts, isUid ? maxUid : totalMessages)) {
|
||
result.push(uidList[i]);
|
||
}
|
||
}
|
||
|
||
return result;
|
||
};
|
||
|
||
module.exports.packMessageRange = function (uidList) {
|
||
if (!Array.isArray(uidList)) {
|
||
uidList = [].concat(uidList || []);
|
||
}
|
||
|
||
if (!uidList.length) {
|
||
return '';
|
||
}
|
||
|
||
uidList.sort((a, b) => (a - b));
|
||
|
||
let last = uidList[uidList.length - 1];
|
||
let result = [
|
||
[last]
|
||
];
|
||
for (let i = uidList.length - 2; i >= 0; i--) {
|
||
if (uidList[i] === uidList[i + 1] - 1) {
|
||
result[0].unshift(uidList[i]);
|
||
continue;
|
||
}
|
||
result.unshift([uidList[i]]);
|
||
}
|
||
|
||
result = result.map(item => {
|
||
if (item.length === 1) {
|
||
return item[0];
|
||
}
|
||
return item.shift() + ':' + item.pop();
|
||
});
|
||
|
||
return result.join(',');
|
||
};
|
||
|
||
/**
|
||
* Returns a date in GMT timezone
|
||
*
|
||
* @param {Date} date Date object to parse
|
||
* @returns {String} Internaldate formatted date
|
||
*/
|
||
module.exports.formatInternalDate = function (date) {
|
||
let day = date.getUTCDate(),
|
||
month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
||
][date.getUTCMonth()],
|
||
year = date.getUTCFullYear(),
|
||
hour = date.getUTCHours(),
|
||
minute = date.getUTCMinutes(),
|
||
second = date.getUTCSeconds(),
|
||
tz = 0, //date.getTimezoneOffset(),
|
||
tzHours = Math.abs(Math.floor(tz / 60)),
|
||
tzMins = Math.abs(tz) - tzHours * 60;
|
||
|
||
return (day < 10 ? '0' : '') + day + '-' + month + '-' + year + ' ' +
|
||
(hour < 10 ? '0' : '') + hour + ':' + (minute < 10 ? '0' : '') +
|
||
minute + ':' + (second < 10 ? '0' : '') + second + ' ' +
|
||
(tz > 0 ? '-' : '+') + (tzHours < 10 ? '0' : '') + tzHours +
|
||
(tzMins < 10 ? '0' : '') + tzMins;
|
||
};
|
||
|
||
/**
|
||
* Converts query data and message into an array of query responses.
|
||
*
|
||
* Message object must have the following properties:
|
||
*
|
||
* * raw – string (binary) or buffer with the rfc822 contents of the message
|
||
* * uid – message UID
|
||
* * flags - an array with message flags
|
||
* * date - internaldate date object
|
||
*
|
||
* Additionally the message object *should* have the following properties (if not present then generated automatically):
|
||
*
|
||
* * mimeTree - message MIME tree object
|
||
* * envelope - message IMAP envelope object
|
||
* * bodystructure - message bodustructure object
|
||
* * bodystructureShort - bodyscructure for the BODY query
|
||
*
|
||
* @param {Array} query Query objects
|
||
* @param {Object} message Message object
|
||
* @param {Object} options Options for the indexer
|
||
* @returns {Array} Resolved responses
|
||
*/
|
||
module.exports.getQueryResponse = function (query, message, options) {
|
||
|
||
// for optimization purposes try to use cached mimeTree etc. if available
|
||
// If these values are missing then generate these when first time required
|
||
// So if the query is for (UID FLAGS) then mimeTree is never generated
|
||
let mimeTree = message.mimeTree;
|
||
let indexer = new Indexer(options);
|
||
|
||
// generate response object
|
||
let values = [];
|
||
query.forEach(item => {
|
||
let value = '';
|
||
switch (item.item) {
|
||
|
||
case 'uid':
|
||
value = message.uid;
|
||
break;
|
||
|
||
case 'modseq':
|
||
value = message.modseq;
|
||
break;
|
||
|
||
case 'flags':
|
||
value = message.flags;
|
||
break;
|
||
|
||
case 'internaldate':
|
||
if (!message.internaldate) {
|
||
message.internaldate = new Date();
|
||
}
|
||
value = message.internaldate;
|
||
break;
|
||
|
||
case 'bodystructure':
|
||
if (message.bodystructure) {
|
||
value = message.bodystructure;
|
||
} else {
|
||
if (!mimeTree) {
|
||
mimeTree = indexer.parseMimeTree(message.raw);
|
||
}
|
||
value = indexer.getBodyStructure(mimeTree);
|
||
}
|
||
break;
|
||
|
||
case 'envelope':
|
||
if (message.envelope) {
|
||
value = message.envelope;
|
||
} else {
|
||
if (!mimeTree) {
|
||
mimeTree = indexer.parseMimeTree(message.raw);
|
||
}
|
||
value = indexer.getEnvelope(mimeTree);
|
||
}
|
||
break;
|
||
|
||
case 'rfc822':
|
||
if (!mimeTree) {
|
||
mimeTree = indexer.parseMimeTree(message.raw);
|
||
}
|
||
value = indexer.getContents(mimeTree);
|
||
break;
|
||
|
||
case 'rfc822.size':
|
||
if (message.size) {
|
||
value = message.size;
|
||
} else {
|
||
if (!mimeTree) {
|
||
mimeTree = indexer.parseMimeTree(message.raw);
|
||
}
|
||
value = indexer.getSize(mimeTree);
|
||
}
|
||
break;
|
||
|
||
case 'rfc822.header':
|
||
// Equivalent to BODY[HEADER]
|
||
if (!mimeTree) {
|
||
mimeTree = indexer.parseMimeTree(message.raw);
|
||
}
|
||
value = [].concat(mimeTree.header || []).join('\r\n') + '\r\n\r\n';
|
||
break;
|
||
|
||
case 'rfc822.text':
|
||
// Equivalent to BODY[TEXT]
|
||
if (!mimeTree) {
|
||
mimeTree = indexer.parseMimeTree(message.raw);
|
||
}
|
||
value = indexer.getContents(mimeTree, {
|
||
path: '',
|
||
type: 'text'
|
||
});
|
||
break;
|
||
|
||
case 'body':
|
||
if (!item.hasOwnProperty('type')) {
|
||
// BODY
|
||
if (!mimeTree) {
|
||
mimeTree = indexer.parseMimeTree(message.raw);
|
||
}
|
||
value = indexer.getBody(mimeTree);
|
||
} else if (item.path === '' && item.type === 'content') {
|
||
// BODY[]
|
||
if (!mimeTree) {
|
||
mimeTree = indexer.parseMimeTree(message.raw);
|
||
}
|
||
value = indexer.getContents(mimeTree);
|
||
} else {
|
||
// BODY[SELECTOR]
|
||
if (!mimeTree) {
|
||
mimeTree = indexer.parseMimeTree(message.raw);
|
||
}
|
||
value = indexer.getContents(mimeTree, item);
|
||
}
|
||
|
||
if (item.partial) {
|
||
let len;
|
||
|
||
if (value && value.type === 'stream') {
|
||
value.startFrom = item.partial.startFrom;
|
||
value.maxLength = item.partial.maxLength;
|
||
len = value.expectedLength;
|
||
} else {
|
||
value = value.toString('binary').substr(item.partial.startFrom, item.partial.maxLength);
|
||
len = value.length;
|
||
}
|
||
|
||
// If start+length is larger than available value length, then do not return the length value
|
||
// Instead of BODY[]<10.20> return BODY[]<10> which means that the response is from offset 10 to the end
|
||
if (item.original.partial.length === 2 && (item.partial.maxLength - item.partial.startFrom > len)) {
|
||
item.original.partial.pop();
|
||
}
|
||
}
|
||
|
||
break;
|
||
}
|
||
values.push(value);
|
||
});
|
||
|
||
return values;
|
||
};
|