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