2017-04-25 02:20:06 +08:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// streams through a message body and calculates relaxed body hash
|
|
|
|
|
|
|
|
const Transform = require('stream').Transform;
|
|
|
|
const crypto = require('crypto');
|
|
|
|
|
|
|
|
class DkimStream extends Transform {
|
|
|
|
constructor(options) {
|
|
|
|
super(options);
|
|
|
|
this.chunkBuffer = [];
|
|
|
|
this.chunkBufferLen = 0;
|
|
|
|
this.bodyHash = crypto.createHash('sha256');
|
|
|
|
this.remainder = '';
|
|
|
|
this.byteLength = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
updateHash(chunk) {
|
|
|
|
let bodyStr;
|
|
|
|
|
|
|
|
// find next remainder
|
|
|
|
let nextRemainder = '';
|
|
|
|
|
|
|
|
// This crux finds and removes the spaces from the last line and the newline characters after the last non-empty line
|
|
|
|
// If we get another chunk that does not match this description then we can restore the previously processed data
|
|
|
|
let state = 'file';
|
|
|
|
for (let i = chunk.length - 1; i >= 0; i--) {
|
|
|
|
let c = chunk[i];
|
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
if (state === 'file' && (c === 0x0a || c === 0x0d)) {
|
2017-04-25 02:20:06 +08:00
|
|
|
// do nothing, found \n or \r at the end of chunk, stil end of file
|
|
|
|
} else if (state === 'file' && (c === 0x09 || c === 0x20)) {
|
|
|
|
// switch to line ending mode, this is the last non-empty line
|
|
|
|
state = 'line';
|
|
|
|
} else if (state === 'line' && (c === 0x09 || c === 0x20)) {
|
|
|
|
// do nothing, found ' ' or \t at the end of line, keep processing the last non-empty line
|
|
|
|
} else if (state === 'file' || state === 'line') {
|
|
|
|
// non line/file ending character found, switch to body mode
|
|
|
|
state = 'body';
|
|
|
|
if (i === chunk.length - 1) {
|
|
|
|
// final char is not part of line end or file end, so do nothing
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (i === 0) {
|
|
|
|
// reached to the beginning of the chunk, check if it is still about the ending
|
|
|
|
// and if the remainder also matches
|
2017-06-03 14:51:58 +08:00
|
|
|
if (
|
|
|
|
(state === 'file' && (!this.remainder || /[\r\n]$/.test(this.remainder))) ||
|
|
|
|
(state === 'line' && (!this.remainder || /[ \t]$/.test(this.remainder)))
|
|
|
|
) {
|
2017-04-25 02:20:06 +08:00
|
|
|
// keep everything
|
|
|
|
this.remainder += chunk.toString('binary');
|
|
|
|
return;
|
|
|
|
} else if (state === 'line' || state === 'file') {
|
|
|
|
// process existing remainder as normal line but store the current chunk
|
|
|
|
nextRemainder = chunk.toString('binary');
|
|
|
|
chunk = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (state !== 'body') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// reached first non ending byte
|
|
|
|
nextRemainder = chunk.slice(i + 1).toString('binary');
|
|
|
|
chunk = chunk.slice(0, i + 1);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
let needsFixing = !!this.remainder;
|
|
|
|
if (chunk && !needsFixing) {
|
|
|
|
// check if we even need to change anything
|
|
|
|
for (let i = 0, len = chunk.length; i < len; i++) {
|
2017-06-03 14:51:58 +08:00
|
|
|
if (i && chunk[i] === 0x0a && chunk[i - 1] !== 0x0d) {
|
2017-04-25 02:20:06 +08:00
|
|
|
// missing \r before \n
|
|
|
|
needsFixing = true;
|
|
|
|
break;
|
2017-06-03 14:51:58 +08:00
|
|
|
} else if (i && chunk[i] === 0x0d && chunk[i - 1] === 0x20) {
|
2017-04-25 02:20:06 +08:00
|
|
|
// trailing WSP found
|
|
|
|
needsFixing = true;
|
|
|
|
break;
|
|
|
|
} else if (i && chunk[i] === 0x20 && chunk[i - 1] === 0x20) {
|
|
|
|
// multiple spaces found, needs to be replaced with just one
|
|
|
|
needsFixing = true;
|
|
|
|
break;
|
|
|
|
} else if (chunk[i] === 0x09) {
|
|
|
|
// TAB found, needs to be replaced with a space
|
|
|
|
needsFixing = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (needsFixing) {
|
|
|
|
bodyStr = this.remainder + (chunk ? chunk.toString('binary') : '');
|
|
|
|
this.remainder = nextRemainder;
|
2017-06-03 14:51:58 +08:00
|
|
|
bodyStr = bodyStr
|
|
|
|
.replace(/\r?\n/g, '\n') // use js line endings
|
|
|
|
.replace(/[ \t]*$/gm, '') // remove line endings, rtrim
|
|
|
|
.replace(/[ \t]+/gm, ' ') // single spaces
|
2017-04-25 02:20:06 +08:00
|
|
|
.replace(/\n/g, '\r\n'); // restore rfc822 line endings
|
|
|
|
chunk = Buffer.from(bodyStr, 'binary');
|
|
|
|
} else if (nextRemainder) {
|
|
|
|
this.remainder = nextRemainder;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.debug) {
|
|
|
|
this._debugBody.push(chunk);
|
|
|
|
}
|
|
|
|
this.bodyHash.update(chunk);
|
|
|
|
}
|
|
|
|
|
|
|
|
_transform(chunk, encoding, callback) {
|
|
|
|
if (!chunk || !chunk.length) {
|
|
|
|
return callback();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof chunk === 'string') {
|
2018-05-11 19:39:23 +08:00
|
|
|
chunk = Buffer.from(chunk, encoding);
|
2017-04-25 02:20:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
this.updateHash(chunk);
|
|
|
|
|
|
|
|
this.byteLength += chunk.length;
|
|
|
|
this.push(chunk);
|
|
|
|
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
|
|
|
|
_flush(callback) {
|
|
|
|
// generate final hash and emit it
|
|
|
|
if (/[\r\n]$/.test(this.remainder) && this.byteLength > 2) {
|
|
|
|
// add terminating line end
|
|
|
|
this.bodyHash.update(Buffer.from('\r\n'));
|
|
|
|
}
|
|
|
|
if (!this.byteLength) {
|
|
|
|
// emit empty line buffer to keep the stream flowing
|
|
|
|
this.push(Buffer.from('\r\n'));
|
|
|
|
// this.bodyHash.update(Buffer.from('\r\n'));
|
|
|
|
}
|
|
|
|
this.emit('hash', this.bodyHash.digest('base64'), this.debug ? Buffer.concat(this._debugBody) : false);
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = DkimStream;
|