mailauth
Version:
Email authentication library for Node.js
152 lines (126 loc) • 4.39 kB
JavaScript
'use strict';
// Calculates relaxed body hash for a message body stream
const { Buffer } = require('node:buffer');
const { parseHeaders } = require('../../lib/tools');
const Writable = require('node:stream').Writable;
/**
* Class for separating header from body
*
* @class
* @extends Writable
*/
class MessageParser extends Writable {
constructor(options) {
super(options);
this.byteLength = 0;
this.state = 'header';
this.stateBytes = [];
this.headers = false;
this.headerChunks = [];
}
async nextChunk(/* chunk */) {
// Override in child class
}
async finalChunk() {
// Override in child class
}
async messageHeaders() {
// Override in child class
}
async processChunk(chunk) {
if (!chunk || !chunk.length) {
return;
}
if (this.state === 'header') {
// wait until we have found body part
for (let i = 0; i < chunk.length; i++) {
let c = chunk[i];
this.stateBytes.push(c);
if (this.stateBytes.length > 4) {
this.stateBytes = this.stateBytes.slice(-4);
}
let b0 = this.stateBytes[this.stateBytes.length - 1];
let b1 = this.stateBytes.length > 1 && this.stateBytes[this.stateBytes.length - 2];
let b2 = this.stateBytes.length > 2 && this.stateBytes[this.stateBytes.length - 3];
if (b0 === 0x0a && (b1 === 0x0a || (b1 === 0x0d && b2 === 0x0a))) {
// found header ending
this.state = 'body';
if (i === chunk.length - 1) {
//end of chunk
this.headerChunks.push(chunk);
this.headers = parseHeaders(Buffer.concat(this.headerChunks));
await this.messageHeaders(this.headers);
return;
}
this.headerChunks.push(chunk.slice(0, i + 1));
this.headers = parseHeaders(Buffer.concat(this.headerChunks));
await this.messageHeaders(this.headers);
chunk = chunk.slice(i + 1);
break;
}
}
}
if (this.state !== 'body') {
this.headerChunks.push(chunk);
return;
}
await this.nextChunk(chunk);
}
*ensureLinebreaks(input) {
let pos = 0;
for (let i = 0; i < input.length; i++) {
let c = input[i];
if (c !== 0x0a) {
this.lastByte = c;
} else if (this.lastByte !== 0x0d) {
// emit line break
let buf;
if (i === 0 || pos === i) {
buf = Buffer.from('\r\n');
} else {
buf = Buffer.concat([input.slice(pos, i), Buffer.from('\r\n')]);
}
yield buf;
pos = i + 1;
}
}
if (pos === 0) {
yield input;
} else if (pos < input.length) {
let buf = input.slice(pos);
yield buf;
}
}
async writeAsync(chunk, encoding) {
if (!chunk || !chunk.length) {
return;
}
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk, encoding);
}
for (let partialChunk of this.ensureLinebreaks(chunk)) {
// separate chunk is emitted for every line that uses \n instead of \r\n
await this.processChunk(partialChunk);
this.byteLength += partialChunk.length;
}
}
_write(chunk, encoding, callback) {
this.writeAsync(chunk, encoding)
.then(() => callback())
.catch(err => callback(err));
}
async finish() {
// generate final hash and emit it
await this.finalChunk();
if (!this.headers && this.headerChunks.length) {
this.headers = parseHeaders(Buffer.concat(this.headerChunks));
await this.messageHeaders(this.headers);
}
}
_final(callback) {
this.finish()
.then(() => callback())
.catch(err => callback(err));
}
}
module.exports = { MessageParser };