wildduck/imap-core/lib/commands/fetch.js

304 lines
10 KiB
JavaScript
Raw Normal View History

2017-03-06 05:45:50 +08:00
'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,
2017-03-12 17:48:08 +08:00
messages:messages.length,
2017-03-06 05:45:50 +08:00
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;
}