wildduck/lib/header-splitter.js
2021-03-26 09:58:05 +02:00

170 lines
5.3 KiB
JavaScript

'use strict';
const Transform = require('stream').Transform;
const Headers = require('mailsplit').Headers;
class HeaderSplitter extends Transform {
constructor(options) {
super(options);
this.lastBytes = Buffer.alloc(4);
this.headersParsed = false;
this.headerBytes = 0;
this.headerChunks = [];
this.rawHeaders = false;
this.bodySize = 0;
}
/**
* Keeps count of the last 4 bytes in order to detect line breaks on chunk boundaries
*
* @param {Buffer} data Next data chunk from the stream
*/
_updateLastBytes(data) {
let lblen = this.lastBytes.length;
let nblen = Math.min(data.length, lblen);
// shift existing bytes
for (let i = 0, len = lblen - nblen; i < len; i++) {
this.lastBytes[i] = this.lastBytes[i + nblen];
}
// add new bytes
for (let i = 1; i <= nblen; i++) {
this.lastBytes[lblen - i] = data[data.length - i];
}
}
/**
* Finds and removes message headers from the remaining body for processing.
*
* @param {Buffer} data Next chunk of data
* @return {Boolean} Returns true if headers are already found or false otherwise
*/
async _checkHeaders(data) {
if (this.headersParsed) {
return true;
}
let lblen = this.lastBytes.length;
let headerPos = 0;
this.curLinePos = 0;
for (let i = 0, len = this.lastBytes.length + data.length; i < len; i++) {
let chr;
if (i < lblen) {
chr = this.lastBytes[i];
} else {
chr = data[i - lblen];
}
if (chr === 0x0a && i) {
let pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen];
let pr2 = i > 1 ? (i - 2 < lblen ? this.lastBytes[i - 2] : data[i - 2 - lblen]) : false;
if (pr1 === 0x0a) {
this.headersParsed = true;
headerPos = i - lblen + 1;
this.headerBytes += headerPos;
break;
} else if (pr1 === 0x0d && pr2 === 0x0a) {
this.headersParsed = true;
headerPos = i - lblen + 1;
this.headerBytes += headerPos;
break;
}
}
}
if (this.headersParsed) {
this.headerChunks.push(data.slice(0, headerPos));
this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
this.headers = new Headers(this.rawHeaders);
this.headerChunks = null;
await new Promise(resolve => {
this.emit('headers', {
headers: this.headers,
done: () => {
// emit the processed header
this.push(this.headers.build('\n'));
// emit remaining data chunk
if (data.length - 1 > headerPos) {
let chunk = data.slice(headerPos);
this.bodySize += chunk.length;
this.push(chunk);
}
resolve();
}
});
});
// do not process current chunk yet
return false;
} else {
this.headerBytes += data.length;
this.headerChunks.push(data);
}
// store last 4 bytes to catch header break
this._updateLastBytes(data);
return false;
}
_transform(chunk, encoding, callback) {
if (!chunk || !chunk.length) {
return callback();
}
const processChunk = async () => {
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk, encoding);
}
let headersFound;
try {
headersFound = await this._checkHeaders(chunk);
} catch (E) {
return callback(E);
}
if (headersFound) {
this.bodySize += chunk.length;
this.push(chunk);
}
};
processChunk().then(callback).catch(callback);
}
_flush(callback) {
if (this.headerChunks) {
// all chunks are checked but we did not find where the body starts
// so emit all we got as headers and push empty line as body
this.headersParsed = true;
// add header terminator
this.headerChunks.push(Buffer.from('\n\n'));
this.headerBytes += 4;
// join all chunks into a header block
this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
this.headers = new Headers(this.rawHeaders);
this.headerChunks = null;
return this.emit('headers', {
headers: this.headers,
done: () => {
// emit the processed header
this.push(this.headers.build('\n'));
// this is our body
this.push(Buffer.from('\n'));
callback();
}
});
}
callback();
}
}
module.exports = HeaderSplitter;