wildduck/imap-core/lib/indexer/body-structure.js
2017-03-12 00:48:31 +02:00

273 lines
9.7 KiB
JavaScript

'use strict';
let createEnvelope = require('./create-envelope');
class BodyStructure {
constructor(tree, options) {
this.tree = tree;
this.options = options || {};
this.currentPath = '';
this.bodyStructure = this.createBodystructure(this.tree, this.options);
}
create() {
return this.bodyStructure;
}
/**
* Generates an object out of parsed mime tree, that can be
* serialized into a BODYSTRUCTURE string
*
* @param {Object} tree Parsed mime tree (see mimeparser.js for input)
* @param {Object} [options] Optional options object
* @param {Boolean} [options.contentLanguageString] If true, convert single element array to string for Content-Language
* @param {Boolean} [options.upperCaseKeys] If true, use only upper case key names
* @param {Boolean} [options.skipContentLocation] If true, do not include Content-Location in the output
* @param {Boolean} [options.body] If true, skip extension fields (needed for BODY)
* @param {Object} Object structure in the form of BODYSTRUCTURE
*/
createBodystructure(tree, options) {
options = options || {};
let walker = node => {
switch ((node.parsedHeader['content-type'] || {}).type) {
case 'multipart':
return this.processMultipartNode(node, options);
case 'text':
return this.processTextNode(node, options);
case 'message':
if (node.parsedHeader['content-type'].subtype === 'rfc822') {
if (!options.attachmentRFC822) {
return this.processRFC822Node(node, options);
}
return this.processAttachmentNode(node, options);
}
// fall through
default:
return this.processAttachmentNode(node, options);
}
};
return walker(tree);
}
/**
* Generates a list of basic fields any non-multipart part should have
*
* @param {Object} node A tree node of the parsed mime tree
* @param {Object} [options] Optional options object (see createBodystructure for details)
* @return {Array} A list of basic fields
*/
getBasicFields(node, options) {
let bodyType = node.parsedHeader['content-type'] && node.parsedHeader['content-type'].type || null;
let bodySubtype = node.parsedHeader['content-type'] && node.parsedHeader['content-type'].subtype || null;
let contentTransfer = node.parsedHeader['content-transfer-encoding'] || '7bit';
return [
// body type
options.upperCaseKeys ? bodyType && bodyType.toUpperCase() || null : bodyType,
// body subtype
options.upperCaseKeys ? bodySubtype && bodySubtype.toUpperCase() || null : bodySubtype,
// body parameter parenthesized list
node.parsedHeader['content-type'] &&
node.parsedHeader['content-type'].hasParams &&
this.flatten(Object.keys(node.parsedHeader['content-type'].params).map(key => [
options.upperCaseKeys ? key.toUpperCase() : key,
node.parsedHeader['content-type'].params[key]
])) || null,
// body id
node.parsedHeader['content-id'] || null,
// body description
node.parsedHeader['content-description'] || null,
// body encoding
options.upperCaseKeys ? contentTransfer && contentTransfer.toUpperCase() || '7bit' : contentTransfer,
// body size
node.size
];
}
/**
* Generates a list of extension fields any non-multipart part should have
*
* @param {Object} node A tree node of the parsed mime tree
* @param {Object} [options] Optional options object (see createBodystructure for details)
* @return {Array} A list of extension fields
*/
getExtensionFields(node, options) {
options = options || {};
let languageString = node.parsedHeader['content-language'] &&
node.parsedHeader['content-language'].replace(/[ ,]+/g, ',').replace(/^,+|,+$/g, '');
let language = languageString && languageString.split(',') || null;
let data;
// if `contentLanguageString` is true, then use a string instead of single element array
if (language && language.length === 1 && options.contentLanguageString) {
language = language[0];
}
data = [
// body MD5
node.parsedHeader['content-md5'] || null,
// body disposition
node.parsedHeader['content-disposition'] && [
options.upperCaseKeys ?
node.parsedHeader['content-disposition'].value.toUpperCase() :
node.parsedHeader['content-disposition'].value,
node.parsedHeader['content-disposition'].params &&
node.parsedHeader['content-disposition'].hasParams &&
this.flatten(Object.keys(node.parsedHeader['content-disposition'].params).map(key => [
options.upperCaseKeys ? key.toUpperCase() : key,
node.parsedHeader['content-disposition'].params[key]
])) || null
] || null,
// body language
language
];
// if `skipContentLocation` is true, do not include Content-Location in output
//
// NB! RFC3501 has an errata with content-location type, it is described as
// 'A string list' (eg. an array) in RFC but the errata page states
// that it is a string (http://www.rfc-editor.org/errata_search.php?rfc=3501)
// see note for 'Section 7.4.2, page 75'
if (!options.skipContentLocation) {
// body location
data.push(node.parsedHeader['content-location'] || null);
}
return data;
}
/**
* Processes a node with content-type=multipart/*
*
* @param {Object} node A tree node of the parsed mime tree
* @param {Object} [options] Optional options object (see createBodystructure for details)
* @return {Array} BODYSTRUCTURE for a multipart part
*/
processMultipartNode(node, options) {
options = options || {};
let data = (node.childNodes && node.childNodes.map(tree => this.createBodystructure(tree, options)) || [
[]
]).
concat([
// body subtype
options.upperCaseKeys ? node.multipart && node.multipart.toUpperCase() || null : node.multipart,
// body parameter parenthesized list
node.parsedHeader['content-type'] &&
node.parsedHeader['content-type'].hasParams &&
this.flatten(Object.keys(node.parsedHeader['content-type'].params).map(key => [
options.upperCaseKeys ? key.toUpperCase() : key,
node.parsedHeader['content-type'].params[key]
])) || null
]);
if (options.body) {
return data;
} else {
return data.
// skip body MD5 from extension fields
concat(this.getExtensionFields(node, options).slice(1));
}
}
/**
* Processes a node with content-type=text/*
*
* @param {Object} node A tree node of the parsed mime tree
* @param {Object} [options] Optional options object (see createBodystructure for details)
* @return {Array} BODYSTRUCTURE for a text part
*/
processTextNode(node, options) {
options = options || {};
let data = [].concat(this.getBasicFields(node, options)).concat([
node.lineCount
]);
if (!options.body) {
data = data.concat(this.getExtensionFields(node, options));
}
return data;
}
/**
* Processes a non-text, non-multipart node
*
* @param {Object} node A tree node of the parsed mime tree
* @param {Object} [options] Optional options object (see createBodystructure for details)
* @return {Array} BODYSTRUCTURE for the part
*/
processAttachmentNode(node, options) {
options = options || {};
let data = [].concat(this.getBasicFields(node, options));
if (!options.body) {
data = data.concat(this.getExtensionFields(node, options));
}
return data;
}
/**
* Processes a node with content-type=message/rfc822
*
* @param {Object} node A tree node of the parsed mime tree
* @param {Object} [options] Optional options object (see createBodystructure for details)
* @return {Array} BODYSTRUCTURE for a text part
*/
processRFC822Node(node, options) {
options = options || {};
let data = [].concat(this.getBasicFields(node, options));
data.push(createEnvelope(node.message.parsedHeader));
data.push(this.createBodystructure(node.message, options));
data = data.concat(
node.lineCount
).
concat(this.getExtensionFields(node, options));
return data;
}
/**
* Converts all sub-arrays into one level array
* flatten([1,[2,3]]) -> [1,2,3]
*
* @param {Array} arr An array with possible sub-arrays
* @return {Array} Flat array
*/
flatten(arr) {
let result = [];
if (Array.isArray(arr)) {
arr.forEach(elm => {
if (Array.isArray(elm)) {
result = result.concat(this.flatten(elm));
} else {
result.push(elm);
}
});
} else {
result.push(arr);
}
return result;
}
}
// Expose to the world
module.exports = BodyStructure;