haraka 0.0.33 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.githooks/pre-commit +41 -0
- package/.prettierignore +7 -0
- package/.qlty/.gitignore +7 -0
- package/.qlty/configs/.shellcheckrc +1 -0
- package/.qlty/qlty.toml +15 -0
- package/CHANGELOG.md +1898 -0
- package/CONTRIBUTORS.md +34 -0
- package/Dockerfile +50 -0
- package/LICENSE +22 -0
- package/Plugins.md +227 -0
- package/README.md +119 -4
- package/SECURITY.md +178 -0
- package/TODO +22 -0
- package/bin/haraka +593 -0
- package/bin/haraka_grep +32 -0
- package/config/aliases +2 -0
- package/config/auth_flat_file.ini +7 -0
- package/config/auth_vpopmaild.ini +9 -0
- package/config/connection.ini +79 -0
- package/config/delay_deny.ini +7 -0
- package/config/host_list +3 -0
- package/config/host_list_regex +6 -0
- package/config/http.ini +11 -0
- package/config/lmtp.ini +7 -0
- package/config/log.ini +11 -0
- package/config/outbound.bounce_message +18 -0
- package/config/outbound.bounce_message_html +36 -0
- package/config/outbound.bounce_message_image +106 -0
- package/config/outbound.ini +24 -0
- package/config/plugins +67 -0
- package/config/smtp.ini +37 -0
- package/config/smtp_bridge.ini +4 -0
- package/config/smtp_forward.ini +31 -0
- package/config/smtp_proxy.ini +27 -0
- package/config/tarpit.timeout +1 -0
- package/config/tls.ini +83 -0
- package/config/watch.ini +12 -0
- package/config/xclient.hosts +2 -0
- package/connection.js +1865 -0
- package/contrib/Haraka.cf +6 -0
- package/contrib/Haraka.pm +35 -0
- package/contrib/bad_smtp_server.pl +25 -0
- package/contrib/bsd-rc.d/haraka +63 -0
- package/contrib/debian-init.d/haraka +87 -0
- package/contrib/haraka.init +96 -0
- package/contrib/haraka.service +23 -0
- package/contrib/plugin2npm.sh +81 -0
- package/contrib/ubuntu-upstart/haraka.conf +27 -0
- package/docs/Body.md +1 -0
- package/docs/Config.md +1 -0
- package/docs/Connection.md +153 -0
- package/docs/CoreConfig.md +96 -0
- package/docs/CustomReturnCodes.md +3 -0
- package/docs/HAProxy.md +62 -0
- package/docs/Header.md +1 -0
- package/docs/Logging.md +129 -0
- package/docs/Outbound.md +210 -0
- package/docs/Plugins.md +372 -0
- package/docs/Results.md +7 -0
- package/docs/Transaction.md +135 -0
- package/docs/Tutorial.md +183 -0
- package/docs/deprecated/access.md +3 -0
- package/docs/deprecated/backscatterer.md +9 -0
- package/docs/deprecated/connect.rdns_access.md +53 -0
- package/docs/deprecated/data.headers.md +3 -0
- package/docs/deprecated/data.nomsgid.md +7 -0
- package/docs/deprecated/data.noreceived.md +11 -0
- package/docs/deprecated/data.rfc5322_header_checks.md +11 -0
- package/docs/deprecated/dkim_sign.md +97 -0
- package/docs/deprecated/dkim_verify.md +28 -0
- package/docs/deprecated/dnsbl.md +80 -0
- package/docs/deprecated/dnswl.md +73 -0
- package/docs/deprecated/lookup_rdns.strict.md +67 -0
- package/docs/deprecated/mail_from.access.md +52 -0
- package/docs/deprecated/mail_from.blocklist.md +18 -0
- package/docs/deprecated/mail_from.nobounces.md +8 -0
- package/docs/deprecated/rcpt_to.access.md +53 -0
- package/docs/deprecated/rcpt_to.blocklist.md +18 -0
- package/docs/deprecated/rcpt_to.routes.md +3 -0
- package/docs/deprecated/rdns.regexp.md +30 -0
- package/docs/plugins/aliases.md +3 -0
- package/docs/plugins/auth/auth_bridge.md +34 -0
- package/docs/plugins/auth/auth_ldap.md +4 -0
- package/docs/plugins/auth/auth_proxy.md +36 -0
- package/docs/plugins/auth/auth_vpopmaild.md +33 -0
- package/docs/plugins/auth/flat_file.md +40 -0
- package/docs/plugins/block_me.md +18 -0
- package/docs/plugins/data.signatures.md +11 -0
- package/docs/plugins/delay_deny.md +23 -0
- package/docs/plugins/max_unrecognized_commands.md +6 -0
- package/docs/plugins/prevent_credential_leaks.md +22 -0
- package/docs/plugins/process_title.md +42 -0
- package/docs/plugins/queue/deliver.md +3 -0
- package/docs/plugins/queue/discard.md +32 -0
- package/docs/plugins/queue/lmtp.md +24 -0
- package/docs/plugins/queue/qmail-queue.md +16 -0
- package/docs/plugins/queue/quarantine.md +87 -0
- package/docs/plugins/queue/smtp_bridge.md +32 -0
- package/docs/plugins/queue/smtp_forward.md +127 -0
- package/docs/plugins/queue/smtp_proxy.md +68 -0
- package/docs/plugins/queue/test.md +7 -0
- package/docs/plugins/rcpt_to.in_host_list.md +34 -0
- package/docs/plugins/rcpt_to.max_count.md +3 -0
- package/docs/plugins/record_envelope_addresses.md +20 -0
- package/docs/plugins/relay.md +3 -0
- package/docs/plugins/reseed_rng.md +16 -0
- package/docs/plugins/status.md +41 -0
- package/docs/plugins/tarpit.md +50 -0
- package/docs/plugins/tls.md +235 -0
- package/docs/plugins/toobusy.md +27 -0
- package/docs/plugins/xclient.md +10 -0
- package/docs/tutorials/Migrating_from_v1_to_v2.md +96 -0
- package/docs/tutorials/SettingUpOutbound.md +62 -0
- package/eslint.config.mjs +2 -0
- package/haraka.js +74 -0
- package/haraka.sh +2 -0
- package/http/html/404.html +58 -0
- package/http/html/index.html +47 -0
- package/http/package.json +21 -0
- package/line_socket.js +24 -0
- package/logger.js +322 -0
- package/outbound/client_pool.js +59 -0
- package/outbound/config.js +134 -0
- package/outbound/hmail.js +1504 -0
- package/outbound/index.js +349 -0
- package/outbound/qfile.js +93 -0
- package/outbound/queue.js +399 -0
- package/outbound/tls.js +85 -0
- package/outbound/todo.js +17 -0
- package/package.json +100 -4
- package/plugins/.eslintrc.yaml +3 -0
- package/plugins/auth/auth_base.js +261 -0
- package/plugins/auth/auth_bridge.js +20 -0
- package/plugins/auth/auth_proxy.js +227 -0
- package/plugins/auth/auth_vpopmaild.js +162 -0
- package/plugins/auth/flat_file.js +44 -0
- package/plugins/block_me.js +88 -0
- package/plugins/data.signatures.js +30 -0
- package/plugins/delay_deny.js +153 -0
- package/plugins/prevent_credential_leaks.js +61 -0
- package/plugins/process_title.js +197 -0
- package/plugins/profile.js +11 -0
- package/plugins/queue/deliver.js +12 -0
- package/plugins/queue/discard.js +27 -0
- package/plugins/queue/lmtp.js +45 -0
- package/plugins/queue/qmail-queue.js +93 -0
- package/plugins/queue/quarantine.js +133 -0
- package/plugins/queue/smtp_bridge.js +45 -0
- package/plugins/queue/smtp_forward.js +371 -0
- package/plugins/queue/smtp_proxy.js +142 -0
- package/plugins/queue/test.js +15 -0
- package/plugins/rcpt_to.host_list_base.js +65 -0
- package/plugins/rcpt_to.in_host_list.js +56 -0
- package/plugins/record_envelope_addresses.js +17 -0
- package/plugins/reseed_rng.js +7 -0
- package/plugins/status.js +274 -0
- package/plugins/tarpit.js +45 -0
- package/plugins/tls.js +164 -0
- package/plugins/toobusy.js +47 -0
- package/plugins/xclient.js +124 -0
- package/plugins.js +605 -0
- package/run_tests +11 -0
- package/server.js +827 -0
- package/smtp_client.js +504 -0
- package/test/.eslintrc.yaml +11 -0
- package/test/config/auth_flat_file.ini +5 -0
- package/test/config/block_me.recipient +1 -0
- package/test/config/block_me.senders +1 -0
- package/test/config/dhparams.pem +8 -0
- package/test/config/host_list +2 -0
- package/test/config/outbound_tls_cert.pem +1 -0
- package/test/config/outbound_tls_key.pem +1 -0
- package/test/config/plugins +7 -0
- package/test/config/smtp.ini +11 -0
- package/test/config/smtp_forward.ini +30 -0
- package/test/config/tls/example.com/_.example.com.key +28 -0
- package/test/config/tls/example.com/example.com.crt +25 -0
- package/test/config/tls/haraka.local.pem +51 -0
- package/test/config/tls.ini +45 -0
- package/test/config/tls_cert.pem +21 -0
- package/test/config/tls_key.pem +28 -0
- package/test/connection.js +820 -0
- package/test/fixtures/haproxy_allowed/config/connection.ini +3 -0
- package/test/fixtures/haproxy_disabled/config/connection.ini +3 -0
- package/test/fixtures/haproxy_untrusted/config/connection.ini +3 -0
- package/test/fixtures/line_socket.js +21 -0
- package/test/fixtures/todo_qfile.txt +0 -0
- package/test/fixtures/util_hmailitem.js +156 -0
- package/test/installation/config/test-plugin-flat +1 -0
- package/test/installation/config/test-plugin.ini +10 -0
- package/test/installation/config/tls.ini +1 -0
- package/test/installation/node_modules/load_first/index.js +5 -0
- package/test/installation/node_modules/load_first/package.json +11 -0
- package/test/installation/node_modules/test-plugin/config/test-plugin-flat +1 -0
- package/test/installation/node_modules/test-plugin/config/test-plugin.ini +9 -0
- package/test/installation/node_modules/test-plugin/package.json +5 -0
- package/test/installation/node_modules/test-plugin/test-plugin.js +5 -0
- package/test/installation/plugins/base_plugin.js +3 -0
- package/test/installation/plugins/folder_plugin/index.js +3 -0
- package/test/installation/plugins/folder_plugin/package.json +11 -0
- package/test/installation/plugins/inherits.js +7 -0
- package/test/installation/plugins/load_first.js +3 -0
- package/test/installation/plugins/plugin.js +1 -0
- package/test/installation/plugins/tls.js +3 -0
- package/test/logger.js +217 -0
- package/test/loud/config/dhparams.pem +0 -0
- package/test/loud/config/tls/goobered.pem +45 -0
- package/test/loud/config/tls.ini +43 -0
- package/test/mail_specimen/base64-root-part.txt +23 -0
- package/test/mail_specimen/varied-fold-lengths-preserve-data.txt +283 -0
- package/test/outbound/bounce_net_errors.js +133 -0
- package/test/outbound/bounce_rfc3464.js +226 -0
- package/test/outbound/hmail.js +210 -0
- package/test/outbound/index.js +385 -0
- package/test/outbound/qfile.js +124 -0
- package/test/outbound/queue.js +325 -0
- package/test/plugins/auth/auth_base.js +620 -0
- package/test/plugins/auth/auth_bridge.js +80 -0
- package/test/plugins/auth/auth_vpopmaild.js +81 -0
- package/test/plugins/auth/flat_file.js +123 -0
- package/test/plugins/block_me.js +141 -0
- package/test/plugins/data.signatures.js +111 -0
- package/test/plugins/delay_deny.js +262 -0
- package/test/plugins/prevent_credential_leaks.js +174 -0
- package/test/plugins/process_title.js +141 -0
- package/test/plugins/queue/deliver.js +98 -0
- package/test/plugins/queue/discard.js +78 -0
- package/test/plugins/queue/lmtp.js +137 -0
- package/test/plugins/queue/qmail-queue.js +98 -0
- package/test/plugins/queue/quarantine.js +80 -0
- package/test/plugins/queue/smtp_bridge.js +152 -0
- package/test/plugins/queue/smtp_forward.js +1023 -0
- package/test/plugins/queue/smtp_proxy.js +138 -0
- package/test/plugins/rcpt_to.host_list_base.js +102 -0
- package/test/plugins/rcpt_to.in_host_list.js +186 -0
- package/test/plugins/record_envelope_addresses.js +66 -0
- package/test/plugins/reseed_rng.js +34 -0
- package/test/plugins/status.js +207 -0
- package/test/plugins/tarpit.js +90 -0
- package/test/plugins/tls.js +86 -0
- package/test/plugins/toobusy.js +198 -0
- package/test/plugins/xclient.js +119 -0
- package/test/plugins.js +230 -0
- package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_fixed +0 -0
- package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka +0 -0
- package/test/queue/1508269674999_1508269674999_0_34002_socVUF_1_haraka +0 -0
- package/test/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka +0 -0
- package/test/queue/zero-length +0 -0
- package/test/server.js +1012 -0
- package/test/smtp_client.js +1303 -0
- package/test/tls_socket.js +321 -0
- package/test/transaction.js +554 -0
- package/tls_socket.js +771 -0
- package/transaction.js +267 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
4
|
+
const assert = require('node:assert')
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
|
|
8
|
+
const config = require('haraka-config')
|
|
9
|
+
const transaction = require('../transaction')
|
|
10
|
+
|
|
11
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const endData = (txn) => new Promise((resolve) => txn.end_data(resolve))
|
|
14
|
+
const getData = (stream) => new Promise((resolve) => stream.get_data(resolve))
|
|
15
|
+
|
|
16
|
+
const setUp = () => {
|
|
17
|
+
this.transaction = transaction.createTransaction(undefined, config.get('smtp.ini'))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function addLines(txn, lines) {
|
|
21
|
+
for (const line of lines) txn.add_data(line)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function write_file_data_to_transaction(test_transaction, filename) {
|
|
25
|
+
const specimen = fs.readFileSync(filename, 'utf8')
|
|
26
|
+
const matcher = /[^\n]*([\n]|$)/g
|
|
27
|
+
let line
|
|
28
|
+
do {
|
|
29
|
+
line = matcher.exec(specimen)
|
|
30
|
+
if (line[0] === '') break
|
|
31
|
+
test_transaction.add_data(line[0])
|
|
32
|
+
} while (line[0] !== '')
|
|
33
|
+
test_transaction.end_data()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe('transaction', () => {
|
|
39
|
+
beforeEach(setUp)
|
|
40
|
+
|
|
41
|
+
describe('createTransaction', () => {
|
|
42
|
+
it('generates a UUID when none is provided', () => {
|
|
43
|
+
const txn = transaction.createTransaction()
|
|
44
|
+
assert.ok(txn.uuid, 'uuid is set')
|
|
45
|
+
assert.match(txn.uuid, /^[0-9A-F-]+$/i, 'uuid looks like a UUID')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('uses the provided UUID', () => {
|
|
49
|
+
const txn = transaction.createTransaction('TEST-UUID')
|
|
50
|
+
assert.equal(txn.uuid, 'TEST-UUID')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('initialises header_pos to 0', () => {
|
|
54
|
+
assert.equal(this.transaction.header_pos, 0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('initialises found_hb_sep to false', () => {
|
|
58
|
+
assert.equal(this.transaction.found_hb_sep, false)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('add_body_filter', () => {
|
|
63
|
+
it('filter callback receives correct content-type, encoding, and body', async () => {
|
|
64
|
+
let called = false
|
|
65
|
+
this.transaction.add_body_filter('text/plain', (ct, enc, buf) => {
|
|
66
|
+
assert.ok(ct.startsWith('text/plain'), 'correct content-type')
|
|
67
|
+
assert.match(enc, /utf-?8/i, 'correct encoding')
|
|
68
|
+
assert.equal(buf.toString().trim(), 'Text part', 'correct body text')
|
|
69
|
+
called = true
|
|
70
|
+
})
|
|
71
|
+
addLines(this.transaction, [
|
|
72
|
+
'Content-Type: multipart/alternative; boundary=abcd\n',
|
|
73
|
+
'\n',
|
|
74
|
+
'--abcd\n',
|
|
75
|
+
'Content-Type: text/plain\n',
|
|
76
|
+
'\n',
|
|
77
|
+
'Text part\n',
|
|
78
|
+
'--abcd\n',
|
|
79
|
+
'Content-Type: text/html\n',
|
|
80
|
+
'\n',
|
|
81
|
+
'<p>HTML part</p>\n',
|
|
82
|
+
'--abcd--\n',
|
|
83
|
+
])
|
|
84
|
+
await endData(this.transaction)
|
|
85
|
+
await getData(this.transaction.message_stream)
|
|
86
|
+
assert.ok(called, 'filter was called')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Issue #2290: add_body_filter called after ensure_body() has already run must still apply.
|
|
90
|
+
it('filter applied when added after body already initialised', async () => {
|
|
91
|
+
this.transaction.attachment_hooks(() => {})
|
|
92
|
+
this.transaction.add_data('Content-Type: text/plain\n')
|
|
93
|
+
this.transaction.add_data('\n')
|
|
94
|
+
|
|
95
|
+
let filter_called = false
|
|
96
|
+
this.transaction.add_body_filter('text/plain', (ct, enc, buf) => {
|
|
97
|
+
filter_called = true
|
|
98
|
+
return buf
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
this.transaction.add_data('Hello\n')
|
|
102
|
+
await endData(this.transaction)
|
|
103
|
+
await getData(this.transaction.message_stream)
|
|
104
|
+
assert.ok(filter_called, 'filter called even when added after body init')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('filter added after body init can transform content', async () => {
|
|
108
|
+
this.transaction.attachment_hooks(() => {})
|
|
109
|
+
this.transaction.add_data('Content-Type: text/plain\n')
|
|
110
|
+
this.transaction.add_data('\n')
|
|
111
|
+
|
|
112
|
+
this.transaction.add_body_filter('text/plain', (ct, enc, buf) => {
|
|
113
|
+
return Buffer.from(buf.toString().replace('Hello', 'World'))
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
this.transaction.add_data('Hello\n')
|
|
117
|
+
await endData(this.transaction)
|
|
118
|
+
const body = await getData(this.transaction.message_stream)
|
|
119
|
+
assert.ok(body.toString().includes('World'), 'filter transformed content')
|
|
120
|
+
assert.ok(!body.toString().includes('Hello'), 'original content was replaced')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('filter with regex ct_match fires on matching part', async () => {
|
|
124
|
+
let matched_ct = null
|
|
125
|
+
this.transaction.add_body_filter(/^text\//, (ct, enc, buf) => {
|
|
126
|
+
matched_ct = ct
|
|
127
|
+
return buf
|
|
128
|
+
})
|
|
129
|
+
addLines(this.transaction, [
|
|
130
|
+
'Content-Type: multipart/alternative; boundary=X\n',
|
|
131
|
+
'\n',
|
|
132
|
+
'--X\n',
|
|
133
|
+
'Content-Type: text/plain\n',
|
|
134
|
+
'\n',
|
|
135
|
+
'Plain\n',
|
|
136
|
+
'--X--\n',
|
|
137
|
+
])
|
|
138
|
+
await endData(this.transaction)
|
|
139
|
+
await getData(this.transaction.message_stream)
|
|
140
|
+
assert.ok(matched_ct && matched_ct.startsWith('text/'), 'regex matched content-type')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('attachment_hooks', () => {
|
|
145
|
+
it('sets parse_body to true', () => {
|
|
146
|
+
assert.equal(this.transaction.parse_body, false)
|
|
147
|
+
this.transaction.attachment_hooks(() => {})
|
|
148
|
+
assert.equal(this.transaction.parse_body, true)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('attachment_hooks before set_banner and add_body_filter all cooperate', async () => {
|
|
152
|
+
this.transaction.attachment_hooks(() => {})
|
|
153
|
+
this.transaction.set_banner('banner')
|
|
154
|
+
let filter_called = false
|
|
155
|
+
this.transaction.add_body_filter('', () => {
|
|
156
|
+
filter_called = true
|
|
157
|
+
})
|
|
158
|
+
addLines(this.transaction, ['Content-Type: text/plain\n', '\n', 'Some text\n'])
|
|
159
|
+
await endData(this.transaction)
|
|
160
|
+
const body = await getData(this.transaction.message_stream)
|
|
161
|
+
assert.ok(/banner$/.test(body.toString().trim()), 'banner applied')
|
|
162
|
+
assert.ok(filter_called, 'body filter called')
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('set_banner', () => {
|
|
167
|
+
it('appends text banner to plain-text body', async () => {
|
|
168
|
+
this.transaction.set_banner('TEXT_BANNER', 'HTML_BANNER')
|
|
169
|
+
addLines(this.transaction, ['Content-Type: text/plain\n', '\n', 'Hello\n'])
|
|
170
|
+
await endData(this.transaction)
|
|
171
|
+
const body = await getData(this.transaction.message_stream)
|
|
172
|
+
assert.ok(body.toString().includes('TEXT_BANNER'), 'text banner present')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('appends banners in nested MIME structure', async () => {
|
|
176
|
+
this.transaction.set_banner('TEXT_BANNER', 'HTML_BANNER')
|
|
177
|
+
addLines(this.transaction, [
|
|
178
|
+
'Content-Type: multipart/mixed; boundary="TOP_LEVEL"\r\n',
|
|
179
|
+
'\r\n',
|
|
180
|
+
'--TOP_LEVEL\r\n',
|
|
181
|
+
'Content-Type: multipart/alternative; boundary="INNER_LEVEL"\r\n',
|
|
182
|
+
'\r\n',
|
|
183
|
+
'--INNER_LEVEL\r\n',
|
|
184
|
+
'Content-Type: text/plain; charset=us-ascii\r\n',
|
|
185
|
+
'\r\n',
|
|
186
|
+
'Hello, this is a text part\r\n',
|
|
187
|
+
'--INNER_LEVEL\r\n',
|
|
188
|
+
'Content-Type: text/html; charset=us-ascii\r\n',
|
|
189
|
+
'\r\n',
|
|
190
|
+
'<p>This is an html part</p>\r\n',
|
|
191
|
+
'--INNER_LEVEL--\r\n',
|
|
192
|
+
'--TOP_LEVEL--\r\n',
|
|
193
|
+
])
|
|
194
|
+
await endData(this.transaction)
|
|
195
|
+
const body = await getData(this.transaction.message_stream)
|
|
196
|
+
const str = body.toString()
|
|
197
|
+
assert.ok(/Hello, this is a text part/.test(str), 'text part present')
|
|
198
|
+
assert.ok(/This is an html part/.test(str), 'html part present')
|
|
199
|
+
assert.ok(/TEXT_BANNER/.test(str), 'text banner present')
|
|
200
|
+
assert.ok(/HTML_BANNER/.test(str), 'html banner present')
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('encoding', () => {
|
|
205
|
+
it('correct output when content is non-utf8 (#2176)', async () => {
|
|
206
|
+
// Czech panagram in ISO-8859-2
|
|
207
|
+
const message = Buffer.from([
|
|
208
|
+
0x50, 0xf8, 0xed, 0x6c, 0x69, 0xb9, 0x20, 0xbe, 0x6c, 0x75, 0xbb, 0x6f, 0x76, 0xe8, 0x6b, 0xfd, 0x20,
|
|
209
|
+
0x6b, 0xf9, 0xf2, 0xfa, 0xec, 0x6c, 0x20, 0xef, 0xe2, 0x62, 0x65, 0x6c, 0x73, 0x6b, 0xe9, 0x20, 0xf3,
|
|
210
|
+
0x64, 0x79, 0x2e,
|
|
211
|
+
])
|
|
212
|
+
this.transaction.parse_body = true
|
|
213
|
+
this.transaction.attachment_hooks(() => {})
|
|
214
|
+
addLines(this.transaction, [
|
|
215
|
+
Buffer.from('Content-Type: text/plain; charset=iso-8859-2; format=flowed\n'),
|
|
216
|
+
'\n',
|
|
217
|
+
Buffer.from([...message, 0x0a]),
|
|
218
|
+
])
|
|
219
|
+
await endData(this.transaction)
|
|
220
|
+
const body = await getData(this.transaction.message_stream)
|
|
221
|
+
assert.ok(body.includes(message), 'ISO-8859-2 content not damaged')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('no munging of bytes when not parsing body', async () => {
|
|
225
|
+
// Same Czech panagram — verifies raw pass-through
|
|
226
|
+
const message = Buffer.from([
|
|
227
|
+
0x50, 0xf8, 0xed, 0x6c, 0x69, 0xb9, 0x20, 0xbe, 0x6c, 0x75, 0xbb, 0x6f, 0x76, 0xe8, 0x6b, 0xfd, 0x20,
|
|
228
|
+
0x6b, 0xf9, 0xf2, 0xfa, 0xec, 0x6c, 0x20, 0xef, 0xe2, 0x62, 0x65, 0x6c, 0x73, 0x6b, 0xe9, 0x20, 0xf3,
|
|
229
|
+
0x64, 0x79, 0x2e, 0x0a,
|
|
230
|
+
])
|
|
231
|
+
addLines(this.transaction, ['Content-Type: text/plain; charset=iso-8859-2; format=flowed\n', '\n', message])
|
|
232
|
+
await endData(this.transaction)
|
|
233
|
+
const body = await getData(this.transaction.message_stream)
|
|
234
|
+
assert.ok(body.includes(message), 'raw bytes not damaged')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('add_data auto-converts string input to Buffer', async () => {
|
|
238
|
+
// The code path for string input (should never happen but is defensive)
|
|
239
|
+
this.transaction.add_data('Subject: string-input\n')
|
|
240
|
+
this.transaction.add_data('\n')
|
|
241
|
+
this.transaction.add_data('body\n')
|
|
242
|
+
await endData(this.transaction)
|
|
243
|
+
const body = await getData(this.transaction.message_stream)
|
|
244
|
+
assert.ok(body.toString().includes('string-input'), 'string input was processed')
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
describe('base64 handling', () => {
|
|
249
|
+
it('varied fold-lengths preserve data integrity', async () => {
|
|
250
|
+
const parsed = {}
|
|
251
|
+
const pendingStreams = []
|
|
252
|
+
this.transaction.parse_body = true
|
|
253
|
+
this.transaction.attachment_hooks((ct, filename, body, stream) => {
|
|
254
|
+
pendingStreams.push(
|
|
255
|
+
new Promise((resolve) => {
|
|
256
|
+
let buf = Buffer.alloc(0)
|
|
257
|
+
stream.on('data', (d) => {
|
|
258
|
+
buf = Buffer.concat([buf, d])
|
|
259
|
+
})
|
|
260
|
+
stream.on('end', () => {
|
|
261
|
+
parsed[filename] = buf
|
|
262
|
+
resolve()
|
|
263
|
+
})
|
|
264
|
+
}),
|
|
265
|
+
)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const specimen = path.join(__dirname, 'mail_specimen', 'varied-fold-lengths-preserve-data.txt')
|
|
269
|
+
write_file_data_to_transaction(this.transaction, specimen)
|
|
270
|
+
await Promise.all(pendingStreams)
|
|
271
|
+
|
|
272
|
+
assert.equal(this.transaction.body.children.length, 6)
|
|
273
|
+
|
|
274
|
+
let first = null
|
|
275
|
+
for (const name in parsed) {
|
|
276
|
+
first = first || parsed[name]
|
|
277
|
+
assert.ok(first.equals(parsed[name]), `buffer for '${name}' matches the others`)
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('base64 root HTML decodes correct byte count', () => {
|
|
282
|
+
this.transaction.parse_body = true
|
|
283
|
+
const specimen = path.join(__dirname, 'mail_specimen', 'base64-root-part.txt')
|
|
284
|
+
write_file_data_to_transaction(this.transaction, specimen)
|
|
285
|
+
assert.equal(this.transaction.body.bodytext.length, 425)
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
describe('boundary marker corruption (#2244)', () => {
|
|
290
|
+
it('boundary marker is intact after large folded To header', async () => {
|
|
291
|
+
let buf = ''
|
|
292
|
+
this.transaction.add_data('Content-Type: multipart/alternative; boundary=abcd\r\n')
|
|
293
|
+
buf += 'Content-Type: multipart/alternative; boundary=abcd\r\n'
|
|
294
|
+
this.transaction.add_data(
|
|
295
|
+
'To: "User1_firstname_middlename_lastname" <user1_firstname_middlename_lastname@test.com>,\r\n',
|
|
296
|
+
)
|
|
297
|
+
buf += 'To: "User1_firstname_middlename_lastname" <user1_firstname_middlename_lastname@test.com>,\r\n'
|
|
298
|
+
|
|
299
|
+
// Add enough continuation lines to exceed 64 KB
|
|
300
|
+
for (let i = 0; i < 725; i++) {
|
|
301
|
+
const line = ` "User${i}_fn_mn_ln" <user${i}_fn_mn_ln@test.com>,\r\n`
|
|
302
|
+
this.transaction.add_data(line)
|
|
303
|
+
buf += line
|
|
304
|
+
}
|
|
305
|
+
const last = ' "Final_User_fn_mn_ln" <final_user_fn_mn_ln@test.com>\r\n'
|
|
306
|
+
this.transaction.add_data(last)
|
|
307
|
+
buf += last
|
|
308
|
+
this.transaction.add_data('Message-ID: <Boundary_Marker_Test>\r\n')
|
|
309
|
+
buf += 'Message-ID: <Boundary_Marker_Test>\r\n'
|
|
310
|
+
this.transaction.add_data('MIME-Version: 1.0\r\n')
|
|
311
|
+
buf += 'MIME-Version: 1.0\r\n'
|
|
312
|
+
this.transaction.add_data('Date: Wed, 1 Jun 2022 16:44:39 +0530\r\n')
|
|
313
|
+
buf += 'Date: Wed, 1 Jun 2022 16:44:39 +0530\r\n'
|
|
314
|
+
this.transaction.add_data('\r\n')
|
|
315
|
+
buf += '\r\n'
|
|
316
|
+
this.transaction.add_data('--abcd\r\n')
|
|
317
|
+
buf += '--abcd\r\n'
|
|
318
|
+
|
|
319
|
+
const rest = [
|
|
320
|
+
'Content-Type: text/plain\r\n',
|
|
321
|
+
'\r\n',
|
|
322
|
+
'Text part\r\n',
|
|
323
|
+
'--abcd\r\n',
|
|
324
|
+
'Content-Type: text/html\r\n',
|
|
325
|
+
'\r\n',
|
|
326
|
+
'<p>HTML part</p>\r\n',
|
|
327
|
+
'--abcd--\r\n',
|
|
328
|
+
]
|
|
329
|
+
for (const line of rest) {
|
|
330
|
+
this.transaction.add_data(line)
|
|
331
|
+
buf += line
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
await endData(this.transaction)
|
|
335
|
+
const body = await getData(this.transaction.message_stream)
|
|
336
|
+
assert.ok(body.includes(Buffer.from(buf)), 'message not damaged')
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
describe('remove_final_cr', () => {
|
|
341
|
+
const cases = [
|
|
342
|
+
{ desc: 'empty buffer', input: '', expected: '' },
|
|
343
|
+
{ desc: 'single byte', input: 'a', expected: 'a' },
|
|
344
|
+
{ desc: 'CRLF ending stripped to LF', input: 'hello\r\n', expected: 'hello\n' },
|
|
345
|
+
{ desc: 'LF-only ending unchanged', input: 'hello\n', expected: 'hello\n' },
|
|
346
|
+
{ desc: 'no newline unchanged', input: 'hello', expected: 'hello' },
|
|
347
|
+
{ desc: 'string input', input: 'hello\r\n', expected: 'hello\n' },
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
for (const { desc, input, expected } of cases) {
|
|
351
|
+
it(desc, () => {
|
|
352
|
+
const result = this.transaction.remove_final_cr(Buffer.from(input))
|
|
353
|
+
assert.equal(result.toString(), expected)
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
describe('add_dot_stuffing_and_ensure_crlf_newlines', () => {
|
|
359
|
+
const cases = [
|
|
360
|
+
{ desc: 'empty string', input: '', expected: '' },
|
|
361
|
+
{ desc: 'no dots or newlines', input: 'hello world', expected: 'hello world' },
|
|
362
|
+
{ desc: 'bare LF becomes CRLF', input: 'hello\n', expected: 'hello\r\n' },
|
|
363
|
+
{ desc: 'CRLF preserved', input: 'hello\r\n', expected: 'hello\r\n' },
|
|
364
|
+
{ desc: 'leading dot stuffed', input: '.hello\n', expected: '..hello\r\n' },
|
|
365
|
+
{ desc: 'mid-line dot not stuffed', input: 'hel.lo\n', expected: 'hel.lo\r\n' },
|
|
366
|
+
{ desc: 'multi-line with leading dots', input: 'a\n.b\n', expected: 'a\r\n..b\r\n' },
|
|
367
|
+
{ desc: 'dot after CRLF stuffed', input: 'a\r\n.b\n', expected: 'a\r\n..b\r\n' },
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
for (const { desc, input, expected } of cases) {
|
|
371
|
+
it(desc, () => {
|
|
372
|
+
const result = this.transaction.add_dot_stuffing_and_ensure_crlf_newlines(Buffer.from(input))
|
|
373
|
+
assert.equal(result.toString(), expected)
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
describe('header manipulation (post-data)', () => {
|
|
379
|
+
it('add_header appends a header', async () => {
|
|
380
|
+
addLines(this.transaction, ['Subject: original\n', '\n', 'body\n'])
|
|
381
|
+
await endData(this.transaction)
|
|
382
|
+
this.transaction.add_header('X-Test', 'added')
|
|
383
|
+
assert.deepEqual(this.transaction.header.get_all('X-Test'), ['added'])
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('add_leading_header prepends a header', async () => {
|
|
387
|
+
addLines(this.transaction, ['Subject: original\n', '\n', 'body\n'])
|
|
388
|
+
await endData(this.transaction)
|
|
389
|
+
this.transaction.add_leading_header('X-Lead', 'first')
|
|
390
|
+
assert.deepEqual(this.transaction.header.get_all('X-Lead'), ['first'])
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('remove_header removes a header', async () => {
|
|
394
|
+
addLines(this.transaction, ['X-Remove: gone\n', '\n', 'body\n'])
|
|
395
|
+
await endData(this.transaction)
|
|
396
|
+
this.transaction.remove_header('X-Remove')
|
|
397
|
+
assert.equal(this.transaction.header.get_all('X-Remove').length, 0)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('add_header appears in message stream output', async () => {
|
|
401
|
+
addLines(this.transaction, ['Subject: original\n', '\n', 'body\n'])
|
|
402
|
+
await endData(this.transaction)
|
|
403
|
+
this.transaction.add_header('X-Added', 'yes')
|
|
404
|
+
const output = (await getData(this.transaction.message_stream)).toString()
|
|
405
|
+
assert.ok(output.includes('X-Added: yes'), 'added header in output')
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('remove_header absent from message stream output', async () => {
|
|
409
|
+
// Keep Subject so header_list stays non-empty after removal; the
|
|
410
|
+
// ctor-headers path then replaces raw headers, omitting X-Remove.
|
|
411
|
+
addLines(this.transaction, ['Subject: Keep\n', 'X-Remove: gone\n', '\n', 'body\n'])
|
|
412
|
+
await endData(this.transaction)
|
|
413
|
+
this.transaction.remove_header('X-Remove')
|
|
414
|
+
const output = (await getData(this.transaction.message_stream)).toString()
|
|
415
|
+
assert.ok(output.includes('Subject: Keep'), 'non-removed header present')
|
|
416
|
+
assert.ok(!output.includes('X-Remove'), 'removed header not in output')
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('folded continuation headers are merged into header_list', async () => {
|
|
420
|
+
addLines(this.transaction, [
|
|
421
|
+
'Subject: This is a very long\n',
|
|
422
|
+
' subject line\n',
|
|
423
|
+
'From: foo@example.com\n',
|
|
424
|
+
'\n',
|
|
425
|
+
'body\n',
|
|
426
|
+
])
|
|
427
|
+
await endData(this.transaction)
|
|
428
|
+
assert.ok(this.transaction.header.get('Subject').includes('long'), 'folded subject parsed')
|
|
429
|
+
assert.ok(this.transaction.header.get('From').includes('foo@example.com'), 'From parsed')
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
describe('pre-data header modifications (e.g. hook_mail / hook_rcpt)', () => {
|
|
434
|
+
it('add_header before data preserves all email headers', async () => {
|
|
435
|
+
// Simulates record_envelope_addresses which calls add_header in hook_mail/hook_rcpt
|
|
436
|
+
// before DATA is received. Must not corrupt header_pos.
|
|
437
|
+
this.transaction.add_header('X-Envelope-From', 'sender@example.com')
|
|
438
|
+
this.transaction.add_header('X-Envelope-To', 'rcpt@example.com')
|
|
439
|
+
|
|
440
|
+
addLines(this.transaction, ['Subject: Test\r\n', 'From: sender@example.com\r\n', '\r\n', 'Body line 1\r\n'])
|
|
441
|
+
await endData(this.transaction)
|
|
442
|
+
|
|
443
|
+
const str = (await getData(this.transaction.message_stream)).toString()
|
|
444
|
+
assert.ok(str.includes('Subject: Test'), 'Subject preserved')
|
|
445
|
+
assert.ok(str.includes('From: sender@example.com'), 'From preserved')
|
|
446
|
+
assert.ok(str.includes('X-Envelope-From: sender@example.com'), 'pre-data header present')
|
|
447
|
+
assert.ok(str.includes('X-Envelope-To: rcpt@example.com'), 'pre-data header present')
|
|
448
|
+
assert.ok(str.includes('Body line 1'), 'body present')
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('add_leading_header before data does not corrupt header_pos', async () => {
|
|
452
|
+
this.transaction.add_leading_header('X-Early', 'value')
|
|
453
|
+
|
|
454
|
+
addLines(this.transaction, ['Subject: Check\r\n', '\r\n', 'body\r\n'])
|
|
455
|
+
await endData(this.transaction)
|
|
456
|
+
|
|
457
|
+
const str = (await getData(this.transaction.message_stream)).toString()
|
|
458
|
+
assert.ok(str.includes('Subject: Check'), 'Subject preserved after add_leading_header')
|
|
459
|
+
assert.ok(str.includes('X-Early: value'), 'pre-data leading header present')
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it('remove_header before data does not corrupt header_pos', async () => {
|
|
463
|
+
// Calling remove_header before data arrives should be a no-op for header_pos
|
|
464
|
+
this.transaction.remove_header('X-Nonexistent')
|
|
465
|
+
|
|
466
|
+
addLines(this.transaction, ['Subject: Check\r\n', '\r\n', 'body\r\n'])
|
|
467
|
+
await endData(this.transaction)
|
|
468
|
+
|
|
469
|
+
const str = (await getData(this.transaction.message_stream)).toString()
|
|
470
|
+
assert.ok(str.includes('Subject: Check'), 'Subject preserved after pre-data remove_header')
|
|
471
|
+
})
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
describe('late header additions (post end_data)', () => {
|
|
475
|
+
it('late add_header to busted email appears before body', async () => {
|
|
476
|
+
addLines(this.transaction, ['Subject: Test\r\n', 'From: user@example.com\r\n', 'Body line 1\r\n'])
|
|
477
|
+
await endData(this.transaction)
|
|
478
|
+
this.transaction.add_header('X-Late', 'true')
|
|
479
|
+
|
|
480
|
+
const str = (await getData(this.transaction.message_stream)).toString()
|
|
481
|
+
assert.ok(str.includes('X-Late: true'), 'late header present')
|
|
482
|
+
assert.ok(str.indexOf('X-Late: true') < str.indexOf('Body line 1'), 'late header before body')
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it('late add_header to clean email appears before body', async () => {
|
|
486
|
+
addLines(this.transaction, ['Subject: Clean\r\n', '\r\n', 'Body line 1\r\n'])
|
|
487
|
+
await endData(this.transaction)
|
|
488
|
+
this.transaction.add_header('X-Late', 'true')
|
|
489
|
+
|
|
490
|
+
const str = (await getData(this.transaction.message_stream)).toString()
|
|
491
|
+
assert.ok(str.includes('X-Late: true'), 'late header present')
|
|
492
|
+
assert.ok(str.indexOf('X-Late: true') < str.indexOf('Body line 1'), 'late header before body')
|
|
493
|
+
})
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
describe('incr_mime_count', () => {
|
|
497
|
+
it('increments mime_part_count', () => {
|
|
498
|
+
assert.equal(this.transaction.mime_part_count, 0)
|
|
499
|
+
this.transaction.incr_mime_count()
|
|
500
|
+
assert.equal(this.transaction.mime_part_count, 1)
|
|
501
|
+
this.transaction.incr_mime_count()
|
|
502
|
+
assert.equal(this.transaction.mime_part_count, 2)
|
|
503
|
+
})
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
describe('discard_data', () => {
|
|
507
|
+
it('end_data calls callback even when discard_data is true', async () => {
|
|
508
|
+
this.transaction.discard_data = true
|
|
509
|
+
addLines(this.transaction, ['Subject: test\n', '\n', 'body\n'])
|
|
510
|
+
await endData(this.transaction) // resolves → callback was called
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it('discard_data with broken email (no separator) calls callback', async () => {
|
|
514
|
+
this.transaction.discard_data = true
|
|
515
|
+
addLines(this.transaction, ['Subject: test\n', 'From: a@b.com\n', 'Body\n'])
|
|
516
|
+
await endData(this.transaction)
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
describe('busted email (no header/body separator)', () => {
|
|
521
|
+
it('headers and body are extracted when separator is missing', async () => {
|
|
522
|
+
addLines(this.transaction, ['Subject: test\n', 'From: a@b.com\n', 'Body line 1\n'])
|
|
523
|
+
await endData(this.transaction)
|
|
524
|
+
|
|
525
|
+
assert.equal(this.transaction.header.get('Subject').trim(), 'test')
|
|
526
|
+
assert.equal(this.transaction.header.get('From').trim(), 'a@b.com')
|
|
527
|
+
|
|
528
|
+
const str = (await getData(this.transaction.message_stream)).toString()
|
|
529
|
+
assert.ok(str.includes('Subject: test'), 'Subject in output')
|
|
530
|
+
assert.ok(str.includes('Body line 1'), 'Body in output')
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it('late add_header to busted email ends up before body in output', async () => {
|
|
534
|
+
addLines(this.transaction, ['Subject: Test\r\n', 'From: user@example.com\r\n', 'Body line 1\r\n'])
|
|
535
|
+
await endData(this.transaction)
|
|
536
|
+
this.transaction.add_header('X-Late', 'true')
|
|
537
|
+
|
|
538
|
+
const str = (await getData(this.transaction.message_stream)).toString()
|
|
539
|
+
assert.ok(str.includes('X-Late: true'), 'late header present')
|
|
540
|
+
assert.ok(str.indexOf('X-Late: true') < str.indexOf('Body line 1'), 'late header before body')
|
|
541
|
+
})
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
describe('parse_body enabled after separator', () => {
|
|
545
|
+
it('does not throw when parse_body set true after separator seen', async () => {
|
|
546
|
+
this.transaction.add_data('Subject: test\n')
|
|
547
|
+
this.transaction.add_data('\n')
|
|
548
|
+
this.transaction.parse_body = true
|
|
549
|
+
assert.doesNotThrow(() => this.transaction.add_data('body line\n'))
|
|
550
|
+
await endData(this.transaction)
|
|
551
|
+
assert.ok(this.transaction.body, 'body was lazily created')
|
|
552
|
+
})
|
|
553
|
+
})
|
|
554
|
+
})
|