wildduck/imap-core/lib/commands/fetch.js
2017-03-12 11:48:08 +02:00

304 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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;
}