mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-01-07 00:17:37 +08:00
303 lines
10 KiB
JavaScript
303 lines
10 KiB
JavaScript
'use strict';
|
||
|
||
let imapTools = require('../imap-tools');
|
||
let imapHandler = require('../handler/imap-handler');
|
||
|
||
/*
|
||
|
||
handles both FETCH and UID FETCH
|
||
|
||
a1 FETCH 1:* (FLAGS BODY BODY.PEEK[HEADER.FIELDS (SUBJECT DATE FROM)] BODY.PEEK[]<0.28> BODY.PEEK[]<0> BODY[HEADER] BODY[1.2])
|
||
a1 FETCH 1 (BODY.PEEK[HEADER] BODY.PEEK[TEXT])
|
||
a1 FETCH 1 (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc bcc message-id in-reply-to references)])
|
||
*/
|
||
|
||
module.exports = {
|
||
state: 'Selected',
|
||
disableNotifications: true,
|
||
|
||
schema: [{
|
||
name: 'range',
|
||
type: 'sequence'
|
||
}, {
|
||
name: 'data',
|
||
type: 'mixed'
|
||
}, {
|
||
name: 'extensions',
|
||
type: 'array',
|
||
optional: true
|
||
}],
|
||
|
||
handler(command, callback) {
|
||
|
||
// Check if FETCH method is set
|
||
if (typeof this._server.onFetch !== 'function') {
|
||
return callback(null, {
|
||
response: 'NO',
|
||
message: command.command + ' not implemented'
|
||
});
|
||
}
|
||
|
||
let isUid = (command.command || '').toString().toUpperCase() === 'UID FETCH' ? true : false;
|
||
let range = command.attributes[0] && command.attributes[0].value || '';
|
||
if (!imapTools.validateSequnce(range)) {
|
||
return callback(new Error('Invalid sequence set for ' + command.command));
|
||
}
|
||
let messages = imapTools.getMessageRange(this.selected.uidList, range, isUid);
|
||
let flagsExist = false;
|
||
let uidExist = false;
|
||
let modseqExist = false;
|
||
let markAsSeen = false;
|
||
let metadataOnly = true;
|
||
let changedSince = 0;
|
||
let query = [];
|
||
|
||
let params = [].concat(command.attributes[1] || []);
|
||
let extensions = [].concat(command.attributes[2] || []).map(val => (val && val.value));
|
||
|
||
if (extensions.length) {
|
||
if (extensions.length !== 2 || (extensions[0] || '').toString().toUpperCase() !== 'CHANGEDSINCE' || isNaN(extensions[1])) {
|
||
return callback(new Error('Invalid modifier for ' + command.command));
|
||
}
|
||
changedSince = Number(extensions[1]);
|
||
if (changedSince && !this.selected.condstoreEnabled) {
|
||
this.condstoreEnabled = this.selected.condstoreEnabled = true;
|
||
}
|
||
}
|
||
|
||
let macros = new Map(
|
||
// Map iterator is a list of tuples
|
||
[
|
||
// ALL
|
||
['ALL', ['FLAGS', 'INTERNALDATE', 'RFC822.SIZE', 'ENVELOPE']],
|
||
// FAST
|
||
['FAST', ['FLAGS', 'INTERNALDATE', 'RFC822.SIZE']],
|
||
// FULL
|
||
['FULL', ['FLAGS', 'INTERNALDATE', 'RFC822.SIZE', 'ENVELOPE', 'BODY']]
|
||
]
|
||
);
|
||
|
||
let i, len, param, section;
|
||
|
||
// normalize query
|
||
|
||
// replace macro with actual items
|
||
if (command.attributes[1].type === 'ATOM' && macros.has(command.attributes[1].value.toUpperCase())) {
|
||
params = macros.get(command.attributes[1].value.toUpperCase());
|
||
}
|
||
|
||
// checks conditions – does the messages need to be marked as seen, is the full body needed etc.
|
||
for (i = 0, len = params.length; i < len; i++) {
|
||
param = params[i];
|
||
if (!param || (typeof param !== 'string' && param.type !== 'ATOM')) {
|
||
return callback(new Error('Invalid message data item name for ' + command.command));
|
||
}
|
||
|
||
if (typeof param === 'string') {
|
||
param = params[i] = {
|
||
type: 'ATOM',
|
||
value: param
|
||
};
|
||
}
|
||
|
||
if (param.value.toUpperCase() === 'FLAGS') {
|
||
flagsExist = true;
|
||
}
|
||
|
||
if (param.value.toUpperCase() === 'UID') {
|
||
uidExist = true;
|
||
}
|
||
|
||
if (param.value.toUpperCase() === 'MODSEQ') {
|
||
modseqExist = true;
|
||
}
|
||
|
||
if (!this.selected.readOnly) {
|
||
if (param.value.toUpperCase() === 'BODY' && param.section) {
|
||
// BODY[...]
|
||
markAsSeen = true;
|
||
} else if (param.value.toUpperCase() === 'RFC822') {
|
||
// RFC822
|
||
markAsSeen = true;
|
||
}
|
||
}
|
||
|
||
if (param.value.toUpperCase() === 'BODY.PEEK' && param.section) {
|
||
param.value = 'BODY';
|
||
}
|
||
|
||
if (['BODY', 'RFC822', 'RFC822.SIZE', 'RFC822.HEADER', 'RFC822.TEXT', 'BODYSTRUCTURE'].indexOf(param.value.toUpperCase()) >= 0) {
|
||
metadataOnly = false;
|
||
}
|
||
}
|
||
|
||
// Adds FLAGS to the response if needed. If the query touches BODY[] then this message
|
||
// must be marked as \Seen. To inform the client about flags change, include the updated
|
||
// flags in the response
|
||
if (markAsSeen && !flagsExist) {
|
||
params.push({
|
||
type: 'ATOM',
|
||
value: 'FLAGS'
|
||
});
|
||
}
|
||
|
||
// ensure UID is listed if the command is UID FETCH
|
||
if (isUid && !uidExist) {
|
||
params.push({
|
||
type: 'ATOM',
|
||
value: 'UID'
|
||
});
|
||
}
|
||
|
||
// ensure MODSEQ is listed if the command uses CHANGEDSINCE modifier
|
||
if (changedSince && !modseqExist) {
|
||
params.push({
|
||
type: 'ATOM',
|
||
value: 'MODSEQ'
|
||
});
|
||
}
|
||
|
||
// returns header field name from a IMAP command object
|
||
let getFieldName = field => (field.value || '').toString().toLowerCase();
|
||
|
||
// compose query object from parsed IMAP command
|
||
for (i = 0, len = params.length; i < len; i++) {
|
||
param = params[i];
|
||
let item = {
|
||
query: imapHandler.compiler({
|
||
attributes: param
|
||
}),
|
||
item: (param.value || '').toString().toLowerCase(),
|
||
original: param
|
||
};
|
||
|
||
if (param.section) {
|
||
if (!param.section.length) {
|
||
item.path = '';
|
||
item.type = 'content';
|
||
} else {
|
||
// we are expecting stuff like 'TEXT' or '1.2.3.TEXT' or '1.2.3'
|
||
// the numeric part ('1.2.3') is the path to the MIME node
|
||
// and 'TEXT' or '' is the queried item (empty means entire content)
|
||
section = (param.section[0].value || '').toString().toLowerCase();
|
||
item.path = section.match(/^(\d+\.)*(\d+$)?/);
|
||
|
||
if (item.path && item.path[0].length) {
|
||
item.path = item.path[0].replace(/\.$/, '');
|
||
item.type = section.substr(item.path.length + 1) || 'content';
|
||
} else {
|
||
item.path = isNaN(section) ? '' : section;
|
||
item.type = section;
|
||
}
|
||
|
||
/*
|
||
item.type = (param.section[0].value || '').toString().toLowerCase();
|
||
*/
|
||
if (/^HEADER.FIELDS(\.NOT)?$/i.test(item.type) && Array.isArray(param.section[1])) {
|
||
item.headers = param.section[1].map(getFieldName);
|
||
}
|
||
}
|
||
// return this element as literal value
|
||
item.isLiteral = true;
|
||
}
|
||
|
||
if (['RFC822', 'RFC822.HEADER', 'RFC822.TEXT'].indexOf(param.value.toUpperCase()) >= 0) {
|
||
item.isLiteral = true;
|
||
}
|
||
|
||
if (param.partial) {
|
||
item.partial = {
|
||
startFrom: Number(param.partial[0]) || 0,
|
||
maxLength: Number(param.partial[1]) || 0
|
||
};
|
||
}
|
||
if (!imapTools.fetchSchema.hasOwnProperty(item.item) || !checkSchema(imapTools.fetchSchema[item.item], item)) {
|
||
return callback(null, {
|
||
response: 'BAD',
|
||
message: 'Invalid message data item ' + item.query + ' for ' + command.command
|
||
});
|
||
}
|
||
|
||
query.push(item);
|
||
}
|
||
|
||
this._server.logger.debug('[%s] FETCH: %s', this.id, JSON.stringify({
|
||
metadataOnly: !!metadataOnly,
|
||
markAsSeen: !!markAsSeen,
|
||
messages:messages.length,
|
||
query,
|
||
changedSince,
|
||
isUid
|
||
}));
|
||
|
||
this._server.onFetch(this.selected.mailbox, {
|
||
metadataOnly: !!metadataOnly,
|
||
markAsSeen: !!markAsSeen,
|
||
messages,
|
||
query,
|
||
changedSince,
|
||
isUid
|
||
}, this.session, (err, success) => {
|
||
if (err) {
|
||
return callback(err);
|
||
}
|
||
|
||
callback(null, {
|
||
response: success === true ? 'OK' : 'NO',
|
||
code: typeof success === 'string' ? success.toUpperCase() : false
|
||
});
|
||
});
|
||
}
|
||
};
|
||
|
||
function checkSchema(schema, item) {
|
||
let i, len;
|
||
if (Array.isArray(schema)) {
|
||
for (i = 0, len = schema.length; i < len; i++) {
|
||
if (checkSchema(schema[i], item)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
if (schema === true) {
|
||
if (item.hasOwnProperty('type') || item.partial) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
if (typeof schema === 'object' && schema) {
|
||
|
||
// check.type
|
||
switch (Object.prototype.toString.call(schema.type)) {
|
||
case '[object RegExp]':
|
||
if (!schema.type.test(item.type)) {
|
||
return false;
|
||
}
|
||
break;
|
||
case '[object String]':
|
||
if (schema.type !== item.type) {
|
||
return false;
|
||
}
|
||
break;
|
||
case '[object Boolean]':
|
||
if (item.hasOwnProperty('type') || item.partial || schema.type !== true) {
|
||
return false;
|
||
}
|
||
break;
|
||
default:
|
||
return false;
|
||
}
|
||
|
||
// check if headers must be present
|
||
if (schema.headers && schema.headers.test(item.type) && !Array.isArray(item.headers)) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|