Intial Commit
This commit is contained in:
422
nodered/rootfs/data/node_modules/mailsplit/lib/message-splitter.js
generated
vendored
Normal file
422
nodered/rootfs/data/node_modules/mailsplit/lib/message-splitter.js
generated
vendored
Normal file
@@ -0,0 +1,422 @@
|
||||
'use strict';
|
||||
|
||||
const Transform = require('stream').Transform;
|
||||
const MimeNode = require('./mime-node');
|
||||
|
||||
const MAX_HEAD_SIZE = 1 * 1024 * 1024;
|
||||
const MAX_CHILD_NODES = 1000;
|
||||
|
||||
const HEAD = 0x01;
|
||||
const BODY = 0x02;
|
||||
|
||||
class MessageSplitter extends Transform {
|
||||
constructor(config) {
|
||||
let options = {
|
||||
readableObjectMode: true,
|
||||
writableObjectMode: false
|
||||
};
|
||||
super(options);
|
||||
|
||||
this.config = config || {};
|
||||
this.maxHeadSize = this.config.maxHeadSize || MAX_HEAD_SIZE;
|
||||
this.maxChildNodes = this.config.maxChildNodes || MAX_CHILD_NODES;
|
||||
this.tree = [];
|
||||
this.nodeCounter = 0;
|
||||
this.newNode();
|
||||
this.tree.push(this.node);
|
||||
this.line = false;
|
||||
this.errored = false;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, callback) {
|
||||
// process line by line
|
||||
// find next line ending
|
||||
let pos = 0;
|
||||
let i = 0;
|
||||
let group = {
|
||||
type: 'none'
|
||||
};
|
||||
let groupstart = this.line ? -this.line.length : 0;
|
||||
let groupend = 0;
|
||||
|
||||
let checkTrailingLinebreak = data => {
|
||||
if (data.type === 'body' && data.node.parentNode && data.value && data.value.length) {
|
||||
if (data.value[data.value.length - 1] === 0x0a) {
|
||||
groupstart--;
|
||||
groupend--;
|
||||
pos--;
|
||||
if (data.value.length > 1 && data.value[data.value.length - 2] === 0x0d) {
|
||||
groupstart--;
|
||||
groupend--;
|
||||
pos--;
|
||||
if (groupstart < 0 && !this.line) {
|
||||
// store only <CR> as <LF> should be on the positive side
|
||||
this.line = Buffer.allocUnsafe(1);
|
||||
this.line[0] = 0x0d;
|
||||
}
|
||||
data.value = data.value.slice(0, data.value.length - 2);
|
||||
} else {
|
||||
data.value = data.value.slice(0, data.value.length - 1);
|
||||
}
|
||||
} else if (data.value[data.value.length - 1] === 0x0d) {
|
||||
groupstart--;
|
||||
groupend--;
|
||||
pos--;
|
||||
data.value = data.value.slice(0, data.value.length - 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let iterateData = () => {
|
||||
for (let len = chunk.length; i < len; i++) {
|
||||
// find next <LF>
|
||||
if (chunk[i] === 0x0a) {
|
||||
// line end
|
||||
|
||||
let start = Math.max(pos, 0);
|
||||
pos = ++i;
|
||||
|
||||
return this.processLine(chunk.slice(start, i), false, (err, data, flush) => {
|
||||
if (err) {
|
||||
this.errored = true;
|
||||
return setImmediate(() => callback(err));
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return setImmediate(iterateData);
|
||||
}
|
||||
|
||||
if (flush) {
|
||||
if (group && group.type !== 'none') {
|
||||
if (group.type === 'body' && groupend >= groupstart && group.node.parentNode) {
|
||||
// do not include the last line ending for body
|
||||
if (chunk[groupend - 1] === 0x0a) {
|
||||
groupend--;
|
||||
if (groupend >= groupstart && chunk[groupend - 1] === 0x0d) {
|
||||
groupend--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (groupstart !== groupend) {
|
||||
group.value = chunk.slice(groupstart, groupend);
|
||||
if (groupend < i) {
|
||||
data.value = chunk.slice(groupend, i);
|
||||
}
|
||||
}
|
||||
this.push(group);
|
||||
group = {
|
||||
type: 'none'
|
||||
};
|
||||
groupstart = groupend = i;
|
||||
}
|
||||
this.push(data);
|
||||
groupend = i;
|
||||
return setImmediate(iterateData);
|
||||
}
|
||||
|
||||
if (data.type === group.type) {
|
||||
// shift slice end position forward
|
||||
groupend = i;
|
||||
} else {
|
||||
if (group.type === 'body' && groupend >= groupstart && group.node.parentNode) {
|
||||
// do not include the last line ending for body
|
||||
if (chunk[groupend - 1] === 0x0a) {
|
||||
groupend--;
|
||||
if (groupend >= groupstart && chunk[groupend - 1] === 0x0d) {
|
||||
groupend--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (group.type !== 'none' && group.type !== 'node') {
|
||||
// we have a previous data/body chunk to output
|
||||
if (groupstart !== groupend) {
|
||||
group.value = chunk.slice(groupstart, groupend);
|
||||
if (group.value && group.value.length) {
|
||||
this.push(group);
|
||||
group = {
|
||||
type: 'none'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === 'node') {
|
||||
this.push(data);
|
||||
groupstart = i;
|
||||
groupend = i;
|
||||
} else if (groupstart < 0) {
|
||||
groupstart = i;
|
||||
groupend = i;
|
||||
checkTrailingLinebreak(data);
|
||||
if (data.value && data.value.length) {
|
||||
this.push(data);
|
||||
}
|
||||
} else {
|
||||
// start new body/data chunk
|
||||
group = data;
|
||||
groupstart = groupend;
|
||||
groupend = i;
|
||||
}
|
||||
}
|
||||
return setImmediate(iterateData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// skip last linebreak for body
|
||||
if (pos >= groupstart + 1 && group.type === 'body' && group.node.parentNode) {
|
||||
// do not include the last line ending for body
|
||||
if (chunk[pos - 1] === 0x0a) {
|
||||
pos--;
|
||||
if (pos >= groupstart && chunk[pos - 1] === 0x0d) {
|
||||
pos--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (group.type !== 'none' && group.type !== 'node' && pos > groupstart) {
|
||||
// we have a leftover data/body chunk to push out
|
||||
group.value = chunk.slice(groupstart, pos);
|
||||
|
||||
if (group.value && group.value.length) {
|
||||
this.push(group);
|
||||
group = {
|
||||
type: 'none'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (pos < chunk.length) {
|
||||
if (this.line) {
|
||||
this.line = Buffer.concat([this.line, chunk.slice(pos)]);
|
||||
} else {
|
||||
this.line = chunk.slice(pos);
|
||||
}
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
setImmediate(iterateData);
|
||||
}
|
||||
|
||||
_flush(callback) {
|
||||
if (this.errored) {
|
||||
return callback();
|
||||
}
|
||||
this.processLine(false, true, (err, data) => {
|
||||
if (err) {
|
||||
return setImmediate(() => callback(err));
|
||||
}
|
||||
if (data && (data.type === 'node' || (data.value && data.value.length))) {
|
||||
this.push(data);
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
compareBoundary(line, startpos, boundary) {
|
||||
// --{boundary}\r\n or --{boundary}--\r\n
|
||||
if (line.length < boundary.length + 3 + startpos || line.length > boundary.length + 6 + startpos) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < boundary.length; i++) {
|
||||
if (line[i + 2 + startpos] !== boundary[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let pos = 0;
|
||||
for (let i = boundary.length + 2 + startpos; i < line.length; i++) {
|
||||
let c = line[i];
|
||||
if (pos === 0 && (c === 0x0d || c === 0x0a)) {
|
||||
// 1: next node
|
||||
return 1;
|
||||
}
|
||||
if (pos === 0 && c !== 0x2d) {
|
||||
// expecting "-"
|
||||
return false;
|
||||
}
|
||||
if (pos === 1 && c !== 0x2d) {
|
||||
// expecting "-"
|
||||
return false;
|
||||
}
|
||||
if (pos === 2 && c !== 0x0d && c !== 0x0a) {
|
||||
// expecting line terminator, either <CR> or <LF>
|
||||
return false;
|
||||
}
|
||||
if (pos === 3 && c !== 0x0a) {
|
||||
// expecting line terminator <LF>
|
||||
return false;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
// 2: multipart end
|
||||
return 2;
|
||||
}
|
||||
|
||||
checkBoundary(line) {
|
||||
let startpos = 0;
|
||||
if (line.length >= 1 && (line[0] === 0x0d || line[0] === 0x0a)) {
|
||||
startpos++;
|
||||
if (line.length >= 2 && (line[0] === 0x0d || line[1] === 0x0a)) {
|
||||
startpos++;
|
||||
}
|
||||
}
|
||||
if (line.length < 4 || line[startpos] !== 0x2d || line[startpos + 1] !== 0x2d) {
|
||||
// defnitely not a boundary
|
||||
return false;
|
||||
}
|
||||
|
||||
let boundary;
|
||||
if (this.node._boundary && (boundary = this.compareBoundary(line, startpos, this.node._boundary))) {
|
||||
// 1: next child
|
||||
// 2: multipart end
|
||||
return boundary;
|
||||
}
|
||||
|
||||
if (this.node._parentBoundary && (boundary = this.compareBoundary(line, startpos, this.node._parentBoundary))) {
|
||||
// 3: next sibling
|
||||
// 4: parent end
|
||||
return boundary + 2;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
processLine(line, final, next) {
|
||||
let flush = false;
|
||||
|
||||
if (this.line && line) {
|
||||
line = Buffer.concat([this.line, line]);
|
||||
this.line = false;
|
||||
} else if (this.line && !line) {
|
||||
line = this.line;
|
||||
this.line = false;
|
||||
}
|
||||
|
||||
if (!line) {
|
||||
line = Buffer.alloc(0);
|
||||
}
|
||||
|
||||
if (this.nodeCounter > this.maxChildNodes) {
|
||||
let err = new Error('Max allowed child nodes exceeded');
|
||||
err.code = 'EMAXLEN';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// we check boundary outside the HEAD/BODY scope as it may appear anywhere
|
||||
let boundary = this.checkBoundary(line);
|
||||
if (boundary) {
|
||||
// reached boundary, switch context
|
||||
switch (boundary) {
|
||||
case 1:
|
||||
// next child
|
||||
this.newNode(this.node);
|
||||
flush = true;
|
||||
break;
|
||||
case 2:
|
||||
// reached end of children, keep current node
|
||||
break;
|
||||
case 3: {
|
||||
// next sibling
|
||||
let parentNode = this.node.parentNode;
|
||||
if (parentNode && parentNode.contentType === 'message/rfc822') {
|
||||
// special case where immediate parent is an inline message block
|
||||
// move up another step
|
||||
parentNode = parentNode.parentNode;
|
||||
}
|
||||
this.newNode(parentNode);
|
||||
flush = true;
|
||||
break;
|
||||
}
|
||||
case 4:
|
||||
// special case when boundary close a node with only header.
|
||||
if (this.node && this.node._headerlen && !this.node.headers) {
|
||||
this.node.parseHeaders();
|
||||
this.push(this.node);
|
||||
}
|
||||
// move up
|
||||
if (this.tree.length) {
|
||||
this.node = this.tree.pop();
|
||||
}
|
||||
this.state = BODY;
|
||||
break;
|
||||
}
|
||||
|
||||
return next(
|
||||
null,
|
||||
{
|
||||
node: this.node,
|
||||
type: 'data',
|
||||
value: line
|
||||
},
|
||||
flush
|
||||
);
|
||||
}
|
||||
|
||||
switch (this.state) {
|
||||
case HEAD: {
|
||||
this.node.addHeaderChunk(line);
|
||||
if (this.node._headerlen > this.maxHeadSize) {
|
||||
let err = new Error('Max header size for a MIME node exceeded');
|
||||
err.code = 'EMAXLEN';
|
||||
return next(err);
|
||||
}
|
||||
if (final || (line.length === 1 && line[0] === 0x0a) || (line.length === 2 && line[0] === 0x0d && line[1] === 0x0a)) {
|
||||
let currentNode = this.node;
|
||||
|
||||
currentNode.parseHeaders();
|
||||
|
||||
// if the content is attached message then just continue
|
||||
if (
|
||||
currentNode.contentType === 'message/rfc822' &&
|
||||
!this.config.ignoreEmbedded &&
|
||||
(!currentNode.encoding || ['7bit', '8bit', 'binary'].includes(currentNode.encoding)) &&
|
||||
currentNode.disposition !== 'attachment'
|
||||
) {
|
||||
currentNode.messageNode = true;
|
||||
this.newNode(currentNode);
|
||||
if (currentNode.parentNode) {
|
||||
this.node._parentBoundary = currentNode.parentNode._boundary;
|
||||
}
|
||||
} else {
|
||||
if (currentNode.contentType === 'message/rfc822') {
|
||||
currentNode.messageNode = false;
|
||||
}
|
||||
this.state = BODY;
|
||||
if (currentNode.multipart && currentNode._boundary) {
|
||||
this.tree.push(currentNode);
|
||||
}
|
||||
}
|
||||
|
||||
return next(null, currentNode, flush);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
case BODY: {
|
||||
return next(
|
||||
null,
|
||||
{
|
||||
node: this.node,
|
||||
type: this.node.multipart ? 'data' : 'body',
|
||||
value: line
|
||||
},
|
||||
flush
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
next(null, false);
|
||||
}
|
||||
|
||||
newNode(parent) {
|
||||
this.node = new MimeNode(parent || false, this.config);
|
||||
this.state = HEAD;
|
||||
this.nodeCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageSplitter;
|
||||
Reference in New Issue
Block a user