Skip to content

Commit

Permalink
txn: synced with haraka/Haraka
Browse files Browse the repository at this point in the history
  • Loading branch information
msimerson committed Apr 13, 2024
1 parent 6c757ce commit 5e63189
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 47 deletions.
143 changes: 98 additions & 45 deletions lib/transaction.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -42,37 +43,100 @@ 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')
}

ensure_body() {
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))
Expand All @@ -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)
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
"files": [
"CHANGELOG.md",
"lib",
"config",
"CONTRIBUTORS.md"
"config"
],
"engines": {
"node": ">=12"
Expand Down

0 comments on commit 5e63189

Please sign in to comment.