From 5e631892cf10573ac1f982f4aa4eab8345b6892f Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Thu, 11 Apr 2024 11:12:07 -0700 Subject: [PATCH] txn: synced with haraka/Haraka --- lib/transaction.js | 143 +++++++++++++++++++++++++++++++-------------- package.json | 3 +- 2 files changed, 99 insertions(+), 47 deletions(-) diff --git a/lib/transaction.js b/lib/transaction.js index b702afc..f7a5a04 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -1,7 +1,8 @@ // A mock SMTP Transaction +const util = require('util') + const Notes = require('haraka-notes') -const ResultStore = require('haraka-results') const message = require('haraka-email-message') const logger = require('./logger') @@ -42,7 +43,6 @@ class Transaction { this.data_post_delay = 0 this.encoding = 'utf8' this.mime_part_count = 0 - this.results = new ResultStore(this) logger.add_log_methods(this, 'mock-connection') } @@ -50,29 +50,93 @@ class Transaction { if (this.body) return this.body = new message.Body(this.header) - this.attachment_start_hooks.forEach((h) => { - this.body.on('attachment_start', h) - }) + for (const hook of this.attachment_start_hooks) { + this.body.on('attachment_start', h); + } + if (this.banner) this.body.set_banner(this.banner) - this.body_filters.forEach((o) => { + for (const o of this.body_filters) { this.body.add_filter((ct, enc, buf) => { - if ( - (o.ct_match instanceof RegExp && o.ct_match.test(ct.toLowerCase())) || - ct.toLowerCase().indexOf(String(o.ct_match).toLowerCase()) === 0 - ) { - return o.filter(ct, enc, buf) - } + const re_match = (util.types.isRegExp(o.ct_match) && o.ct_match.test(ct.toLowerCase())); + const ct_begins = ct.toLowerCase().indexOf(String(o.ct_match).toLowerCase()) === 0; + if (re_match || ct_begins) return o.filter(ct, enc, buf); }) - }) + } + } + + // Removes the CR of a CRLF newline at the end of the buffer. + remove_final_cr (data) { + if (data.length < 2) return data; + if (!Buffer.isBuffer(data)) data = Buffer.from(data); + + if (data[data.length - 2] === 0x0D && data[data.length - 1] === 0x0A) { + data[data.length - 2] = 0x0A; + return data.slice(0, data.length - 1); + } + return data; + } + + // Duplicates any '.' chars at the beginning of a line (dot-stuffing) and + // ensures all newlines are CRLF. + add_dot_stuffing_and_ensure_crlf_newlines (data) { + if (!data.length) return data; + if (!Buffer.isBuffer(data)) data = Buffer.from(data); + + // Make a new buffer big enough to hold two bytes for every one input + // byte. At most, we add one extra character per input byte, so this + // is always big enough. We allocate it "unsafe" (i.e. no memset) for + // speed because we're about to fill it with data, and the remainder of + // the space we don't fill will be sliced away before we return this. + const output = Buffer.allocUnsafe(data.length * 2); + let output_pos = 0; + + let input_pos = 0; + let next_dot = data.indexOf(0x2E); + let next_lf = data.indexOf(0x0A); + while (next_dot !== -1 || next_lf !== -1) { + const run_end = (next_dot !== -1 && (next_lf === -1 || next_dot < next_lf)) + ? next_dot : next_lf; + + // Copy up till whichever comes first, '.' or '\n' (but don't + // copy the '.' or '\n' itself). + data.copy(output, output_pos, input_pos, run_end); + output_pos += run_end - input_pos; + + if (data[run_end] === 0x2E && (run_end === 0 || data[run_end - 1] === 0x0A)) { + // Replace /^\./ with '..' + output[output_pos++] = 0x2E; + } + else if (data[run_end] === 0x0A && (run_end === 0 || data[run_end - 1] !== 0x0D)) { + // Replace /\r?\n/ with '\r\n' + output[output_pos++] = 0x0D; + } + output[output_pos++] = data[run_end]; + + input_pos = run_end + 1; + + if (run_end === next_dot) { + next_dot = data.indexOf(0x2E, input_pos); + } + else { + next_lf = data.indexOf(0x0A, input_pos); + } + } + + if (input_pos < data.length) { + data.copy(output, output_pos, input_pos); + output_pos += data.length - input_pos; + } + + return output.slice(0, output_pos); } add_data(line) { if (typeof line === 'string') { // This shouldn't ever happen... - line = Buffer.from(line, 'binary') + line = Buffer.from(line, this.encoding); } - // check if this is the end of headers line + // is this the end of headers line? if ( this.header_pos === 0 && (line[0] === 0x0a || (line[0] === 0x0d && line[1] === 0x0a)) @@ -87,23 +151,16 @@ class Transaction { // Build up headers if (this.header_lines.length < this.cfg.headers.max_lines) { if (line[0] === 0x2e) line = line.slice(1) // Strip leading "." - this.header_lines.push(line.toString('binary').replace(/\r\n$/, '\n')) + this.header_lines.push(line.toString(this.encoding).replace(/\r\n$/, '\n')) } } else if (this.parse_body) { - if (line[0] === 0x2e) line = line.slice(1) // Strip leading "." - let new_line = this.body.parse_more( - line.toString('binary').replace(/\r\n$/, '\n'), - ) + let new_line = line; + if (new_line[0] === 0x2E) new_line = new_line.slice(1); // Strip leading "." - if (!new_line.length) { - return // buffering for banners - } - - new_line = new_line - .toString('binary') - .replace(/^\./gm, '..') - .replace(/\r?\n/gm, '\r\n') - line = Buffer.from(new_line, 'binary') + line = this.add_dot_stuffing_and_ensure_crlf_newlines( + this.body.parse_more(this.remove_final_cr(new_line)) + ); + if (!line.length) return; // buffering for banners } if (!this.discard_data) this.message_stream.add_line(line) @@ -127,28 +184,25 @@ class Transaction { this.header_pos = header_pos if (this.parse_body) { this.ensure_body() - for (let j = 0; j < body_lines.length; j++) { - this.body.parse_more(body_lines[j]) + for (const bodyLine of body_lines) { + this.body.parse_more(bodyLine); } } } if (this.header_pos && this.parse_body) { - let data = this.body.parse_end() - if (data.length) { - data = data - .toString('binary') - .replace(/^\./gm, '..') - .replace(/\r?\n/gm, '\r\n') - const line = Buffer.from(data, 'binary') - - if (!this.discard_data) this.message_stream.add_line(line) + + const line = this.add_dot_stuffing_and_ensure_crlf_newlines(this.body.parse_end()); + if (line.length) { + this.body.force_end(); + + if (!this.discard_data) this.message_stream.add_line(line); } } - if (!this.discard_data) { - this.message_stream.add_line_end(cb) - } else { + if (this.discard_data) { cb() + } else { + this.message_stream.add_line_end(cb) } } @@ -194,7 +248,6 @@ class Transaction { exports.Transaction = Transaction -exports.createTransaction = function (uuid, cfg = {}) { - if (!cfg.main) cfg.main = {} +exports.createTransaction = function (uuid, cfg) { return new Transaction(uuid, cfg) } diff --git a/package.json b/package.json index e8720b3..4fbd28c 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,7 @@ "files": [ "CHANGELOG.md", "lib", - "config", - "CONTRIBUTORS.md" + "config" ], "engines": { "node": ">=12"