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,820 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
4
|
+
const assert = require('node:assert/strict')
|
|
5
|
+
|
|
6
|
+
const constants = require('haraka-constants')
|
|
7
|
+
const DSN = require('haraka-dsn')
|
|
8
|
+
const { Address } = require('@haraka/email-address')
|
|
9
|
+
|
|
10
|
+
const connection = require('../connection')
|
|
11
|
+
const Server = require('../server')
|
|
12
|
+
|
|
13
|
+
// Expose SMTP result constants as globals (DENY, DENYSOFT, etc.)
|
|
14
|
+
constants.import(global)
|
|
15
|
+
|
|
16
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function makeClient(opts = {}) {
|
|
19
|
+
return {
|
|
20
|
+
remotePort: opts.remotePort ?? null,
|
|
21
|
+
remoteAddress: opts.remoteAddress ?? null,
|
|
22
|
+
localPort: opts.localPort ?? null,
|
|
23
|
+
localAddress: opts.localAddress ?? null,
|
|
24
|
+
destroy: () => {},
|
|
25
|
+
pause: () => {},
|
|
26
|
+
resume: () => {},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeServer(ip = null) {
|
|
31
|
+
return {
|
|
32
|
+
ip_address: ip,
|
|
33
|
+
address() {
|
|
34
|
+
return this.ip_address
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const setUp = () => {
|
|
40
|
+
this.connection = connection.createConnection(makeClient(), makeServer(), Server.cfg)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
describe('connection', () => {
|
|
46
|
+
describe('initial properties', () => {
|
|
47
|
+
beforeEach(setUp)
|
|
48
|
+
|
|
49
|
+
it('remote object defaults', () => {
|
|
50
|
+
assert.deepEqual(this.connection.remote, {
|
|
51
|
+
ip: null,
|
|
52
|
+
port: null,
|
|
53
|
+
host: null,
|
|
54
|
+
info: null,
|
|
55
|
+
closed: false,
|
|
56
|
+
is_private: false,
|
|
57
|
+
is_local: false,
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('local object defaults', () => {
|
|
62
|
+
assert.equal(this.connection.local.ip, null)
|
|
63
|
+
assert.equal(this.connection.local.port, null)
|
|
64
|
+
assert.ok(this.connection.local.host, 'local.host is set')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('tls object defaults', () => {
|
|
68
|
+
assert.deepEqual(this.connection.tls, {
|
|
69
|
+
enabled: false,
|
|
70
|
+
advertised: false,
|
|
71
|
+
verified: false,
|
|
72
|
+
cipher: {},
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('hello object defaults', () => {
|
|
77
|
+
assert.equal(this.connection.hello.host, null)
|
|
78
|
+
assert.equal(this.connection.hello.verb, null)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('proxy object defaults', () => {
|
|
82
|
+
assert.equal(this.connection.proxy.allowed, false)
|
|
83
|
+
assert.equal(this.connection.proxy.ip, null)
|
|
84
|
+
assert.equal(this.connection.proxy.type, null)
|
|
85
|
+
assert.equal(this.connection.proxy.timer, null)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('notes object exists', () => {
|
|
89
|
+
assert.ok(this.connection.notes, 'notes is set')
|
|
90
|
+
assert.equal(typeof this.connection.notes, 'object')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('transaction is null', () => {
|
|
94
|
+
assert.equal(this.connection.transaction, null)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('capabilities is null', () => {
|
|
98
|
+
assert.equal(this.connection.capabilities, null)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('remote.is_private and remote.is_local default to false', () => {
|
|
102
|
+
assert.equal(this.connection.remote.is_private, false)
|
|
103
|
+
assert.equal(this.connection.remote.is_local, false)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('private IP connection', () => {
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
this.connection = connection.createConnection(
|
|
110
|
+
makeClient({
|
|
111
|
+
remotePort: 2525,
|
|
112
|
+
remoteAddress: '172.16.15.1',
|
|
113
|
+
localPort: 25,
|
|
114
|
+
localAddress: '172.16.15.254',
|
|
115
|
+
}),
|
|
116
|
+
makeServer('172.16.15.254'),
|
|
117
|
+
Server.cfg,
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('remote.is_private is true', () => {
|
|
122
|
+
assert.equal(this.connection.remote.is_private, true)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('remote.is_local is false', () => {
|
|
126
|
+
assert.equal(this.connection.remote.is_local, false)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('remote.port is set', () => {
|
|
130
|
+
assert.equal(this.connection.remote.port, 2525)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('loopback connection', () => {
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
this.connection = connection.createConnection(
|
|
137
|
+
makeClient({ remotePort: 2525, remoteAddress: '127.0.0.2', localPort: 25, localAddress: '172.0.0.1' }),
|
|
138
|
+
makeServer('127.0.0.1'),
|
|
139
|
+
Server.cfg,
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('remote.is_private is true', () => {
|
|
144
|
+
assert.equal(this.connection.remote.is_private, true)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('remote.is_local is true', () => {
|
|
148
|
+
assert.equal(this.connection.remote.is_local, true)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('get_remote', () => {
|
|
153
|
+
beforeEach(setUp)
|
|
154
|
+
|
|
155
|
+
it('formats host and IP', () => {
|
|
156
|
+
this.connection.remote.host = 'a.host.tld'
|
|
157
|
+
this.connection.remote.ip = '172.16.199.198'
|
|
158
|
+
assert.equal(this.connection.get_remote('host'), 'a.host.tld [172.16.199.198]')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('falls back to bracketed IP when no host', () => {
|
|
162
|
+
this.connection.remote.ip = '172.16.199.198'
|
|
163
|
+
assert.equal(this.connection.get_remote('host'), '[172.16.199.198]')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('DNSERROR suppresses hostname', () => {
|
|
167
|
+
this.connection.remote.host = 'DNSERROR'
|
|
168
|
+
this.connection.remote.ip = '172.16.199.198'
|
|
169
|
+
assert.equal(this.connection.get_remote('host'), '[172.16.199.198]')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('NXDOMAIN suppresses hostname', () => {
|
|
173
|
+
this.connection.remote.host = 'NXDOMAIN'
|
|
174
|
+
this.connection.remote.ip = '172.16.199.198'
|
|
175
|
+
assert.equal(this.connection.get_remote('host'), '[172.16.199.198]')
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('local.info', () => {
|
|
180
|
+
beforeEach(setUp)
|
|
181
|
+
|
|
182
|
+
it('contains Haraka/version', () => {
|
|
183
|
+
assert.match(this.connection.local.info, /Haraka\/\d+\.\d+/)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('get_capabilities', () => {
|
|
188
|
+
beforeEach(setUp)
|
|
189
|
+
|
|
190
|
+
it('returns empty array by default', () => {
|
|
191
|
+
assert.deepEqual(this.connection.get_capabilities(), [])
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('relaying', () => {
|
|
196
|
+
beforeEach(setUp)
|
|
197
|
+
|
|
198
|
+
it('defaults to false', () => {
|
|
199
|
+
assert.equal(this.connection.relaying, false)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('set() and get() round-trip on connection', () => {
|
|
203
|
+
this.connection.set('relaying', 'crocodiles')
|
|
204
|
+
assert.equal(this.connection.get('relaying'), 'crocodiles')
|
|
205
|
+
assert.equal(this.connection.relaying, 'crocodiles')
|
|
206
|
+
assert.equal(this.connection._relaying, 'crocodiles')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('direct assignment round-trips', () => {
|
|
210
|
+
this.connection.relaying = 'alligators'
|
|
211
|
+
assert.equal(this.connection.get('relaying'), 'alligators')
|
|
212
|
+
assert.equal(this.connection._relaying, 'alligators')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('set() with a transaction updates txn, not connection', () => {
|
|
216
|
+
this.connection.transaction = {}
|
|
217
|
+
this.connection.set('relaying', 'txn-only')
|
|
218
|
+
assert.equal(this.connection.get('relaying'), 'txn-only')
|
|
219
|
+
assert.equal(this.connection._relaying, false)
|
|
220
|
+
assert.equal(this.connection.transaction._relaying, 'txn-only')
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
describe('get / set', () => {
|
|
225
|
+
beforeEach(setUp)
|
|
226
|
+
|
|
227
|
+
it('sets and gets a single-level property', () => {
|
|
228
|
+
this.connection.set('encoding', true)
|
|
229
|
+
assert.ok(this.connection.encoding)
|
|
230
|
+
assert.ok(this.connection.get('encoding'))
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('sets and gets a two-level property', () => {
|
|
234
|
+
this.connection.set('local.host', 'test')
|
|
235
|
+
assert.equal(this.connection.local.host, 'test')
|
|
236
|
+
assert.equal(this.connection.get('local.host'), 'test')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('sets and gets a three-level property', () => {
|
|
240
|
+
this.connection.set('some.fine.example', true)
|
|
241
|
+
assert.ok(this.connection.some.fine.example)
|
|
242
|
+
assert.ok(this.connection.get('some.fine.example'))
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('sets hello.verb via set()', () => {
|
|
246
|
+
this.connection.set('hello', 'verb', 'EHLO')
|
|
247
|
+
assert.equal(this.connection.hello.verb, 'EHLO')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('sets proxy fields via set()', () => {
|
|
251
|
+
this.connection.set('proxy', 'ip', '172.16.15.1')
|
|
252
|
+
this.connection.set('proxy', 'type', 'haproxy')
|
|
253
|
+
this.connection.set('proxy', 'allowed', true)
|
|
254
|
+
assert.equal(this.connection.proxy.ip, '172.16.15.1')
|
|
255
|
+
assert.equal(this.connection.proxy.type, 'haproxy')
|
|
256
|
+
assert.equal(this.connection.proxy.allowed, true)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('has normalised connection properties after set()', () => {
|
|
260
|
+
this.connection.set('remote', 'ip', '172.16.15.1')
|
|
261
|
+
this.connection.set('hello', 'verb', 'EHLO')
|
|
262
|
+
this.connection.set('tls', 'enabled', true)
|
|
263
|
+
assert.equal(this.connection.remote.ip, '172.16.15.1')
|
|
264
|
+
assert.equal(this.connection.remote.port, null)
|
|
265
|
+
assert.equal(this.connection.hello.verb, 'EHLO')
|
|
266
|
+
assert.equal(this.connection.hello.host, null)
|
|
267
|
+
assert.equal(this.connection.tls.enabled, true)
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe('queue_msg', () => {
|
|
272
|
+
beforeEach(setUp)
|
|
273
|
+
|
|
274
|
+
it('returns supplied message when given', () => {
|
|
275
|
+
assert.equal(this.connection.queue_msg(1, 'test message'), 'test message')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('returns default DENY message', () => {
|
|
279
|
+
assert.equal(this.connection.queue_msg(DENY), 'Message denied')
|
|
280
|
+
assert.equal(this.connection.queue_msg(DENYDISCONNECT), 'Message denied')
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('returns default DENYSOFT message', () => {
|
|
284
|
+
assert.equal(this.connection.queue_msg(DENYSOFT), 'Message denied temporarily')
|
|
285
|
+
assert.equal(this.connection.queue_msg(DENYSOFTDISCONNECT), 'Message denied temporarily')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('returns empty string for unrecognised code', () => {
|
|
289
|
+
assert.equal(this.connection.queue_msg('hello'), '')
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
describe('respond', () => {
|
|
294
|
+
beforeEach(setUp)
|
|
295
|
+
|
|
296
|
+
it('returns undefined when disconnected', () => {
|
|
297
|
+
this.connection.state = constants.connection.state.DISCONNECTED
|
|
298
|
+
assert.equal(this.connection.respond(200, 'your lucky day'), undefined)
|
|
299
|
+
assert.equal(this.connection.respond(550, 'you are jacked'), undefined)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('formats a simple 200 response', () => {
|
|
303
|
+
assert.equal(this.connection.respond(200, 'you may pass Go'), '200 you may pass Go\r\n')
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('formats a DSN 200 response', () => {
|
|
307
|
+
assert.equal(
|
|
308
|
+
this.connection.respond(200, DSN.create(200, 'you may pass Go')),
|
|
309
|
+
'200 2.0.0 you may pass Go\r\n',
|
|
310
|
+
)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('DSN overrides response code', () => {
|
|
314
|
+
assert.equal(
|
|
315
|
+
this.connection.respond(450, DSN.create(550, 'This domain is not in use')),
|
|
316
|
+
'550 5.0.0 This domain is not in use\r\n',
|
|
317
|
+
)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('DSN addr_bad_dest_system (5.1.2)', () => {
|
|
321
|
+
assert.equal(
|
|
322
|
+
this.connection.respond(550, DSN.addr_bad_dest_system('Domain not in use', 550)),
|
|
323
|
+
'550 5.1.2 Domain not in use\r\n',
|
|
324
|
+
)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('formats multi-line response from array', () => {
|
|
328
|
+
const resp = this.connection.respond(250, ['Hello', 'World'])
|
|
329
|
+
assert.ok(resp.includes('250-Hello\r\n'), 'first line uses dash')
|
|
330
|
+
assert.ok(resp.includes('250 World\r\n'), 'last line uses space')
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('formats multi-line response from newline-separated string', () => {
|
|
334
|
+
const resp = this.connection.respond(250, 'Hello\nWorld')
|
|
335
|
+
assert.ok(resp.includes('250-Hello\r\n'), 'first line uses dash')
|
|
336
|
+
assert.ok(resp.includes('250 World\r\n'), 'last line uses space')
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('last_response is updated when client has a write method', () => {
|
|
340
|
+
// When client.write is defined, respond() writes to the socket and
|
|
341
|
+
// stores the formatted buffer in last_response.
|
|
342
|
+
let written = ''
|
|
343
|
+
this.connection.client.write = (buf) => {
|
|
344
|
+
written += buf
|
|
345
|
+
}
|
|
346
|
+
this.connection.respond(250, 'OK')
|
|
347
|
+
assert.ok(written.includes('250 OK'), 'data written to socket')
|
|
348
|
+
assert.ok(this.connection.last_response.includes('250 OK'), 'last_response updated')
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
describe('pause and resume', () => {
|
|
353
|
+
beforeEach(setUp)
|
|
354
|
+
|
|
355
|
+
it('restores previous state when still paused at resume', () => {
|
|
356
|
+
this.connection.state = constants.connection.state.PAUSE_SMTP
|
|
357
|
+
this.connection.pause()
|
|
358
|
+
this.connection.resume()
|
|
359
|
+
assert.equal(this.connection.state, constants.connection.state.PAUSE_SMTP)
|
|
360
|
+
assert.equal(this.connection.prev_state, null)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('does not overwrite state changed while paused', () => {
|
|
364
|
+
this.connection.state = constants.connection.state.PAUSE_SMTP
|
|
365
|
+
this.connection.pause()
|
|
366
|
+
this.connection.state = constants.connection.state.CMD
|
|
367
|
+
this.connection.resume()
|
|
368
|
+
assert.equal(this.connection.state, constants.connection.state.CMD)
|
|
369
|
+
assert.equal(this.connection.prev_state, null)
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
describe('loop_respond', () => {
|
|
374
|
+
beforeEach(setUp)
|
|
375
|
+
|
|
376
|
+
it('sets state to LOOP', () => {
|
|
377
|
+
this.connection.loop_respond(554, 'Denied')
|
|
378
|
+
assert.equal(this.connection.state, constants.connection.state.LOOP)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('records loop_code and loop_msg', () => {
|
|
382
|
+
this.connection.loop_respond(554, 'Denied')
|
|
383
|
+
assert.equal(this.connection.loop_code, 554)
|
|
384
|
+
assert.equal(this.connection.loop_msg, 'Denied')
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('does nothing when already disconnecting', () => {
|
|
388
|
+
this.connection.state = constants.connection.state.DISCONNECTING
|
|
389
|
+
this.connection.loop_respond(554, 'Denied')
|
|
390
|
+
assert.equal(this.connection.state, constants.connection.state.DISCONNECTING)
|
|
391
|
+
})
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
describe('tran_uuid', () => {
|
|
395
|
+
beforeEach(setUp)
|
|
396
|
+
|
|
397
|
+
it('increments tran_count on each call', () => {
|
|
398
|
+
assert.equal(this.connection.tran_count, 0)
|
|
399
|
+
const u1 = this.connection.tran_uuid()
|
|
400
|
+
assert.equal(this.connection.tran_count, 1)
|
|
401
|
+
const u2 = this.connection.tran_uuid()
|
|
402
|
+
assert.equal(this.connection.tran_count, 2)
|
|
403
|
+
assert.notEqual(u1, u2)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('formats as <connection-uuid>.<count>', () => {
|
|
407
|
+
const u = this.connection.tran_uuid()
|
|
408
|
+
assert.match(u, new RegExp(`^${this.connection.uuid}\\.1$`))
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
describe('issue #3374 — double QUIT prevention', () => {
|
|
413
|
+
beforeEach(setUp)
|
|
414
|
+
|
|
415
|
+
it('quit hook fires only once when two QUITs arrive in LOOP state', async () => {
|
|
416
|
+
const conn = this.connection
|
|
417
|
+
conn.loop_respond(554, 'Denied')
|
|
418
|
+
assert.equal(conn.state, constants.connection.state.LOOP)
|
|
419
|
+
|
|
420
|
+
let quit_hook_calls = 0
|
|
421
|
+
const plugins = require('../plugins')
|
|
422
|
+
const original_run_hooks = plugins.run_hooks
|
|
423
|
+
plugins.run_hooks = (hook, c, params) => {
|
|
424
|
+
if (hook === 'quit') {
|
|
425
|
+
quit_hook_calls++
|
|
426
|
+
if (quit_hook_calls === 1) {
|
|
427
|
+
setTimeout(() => c.quit_respond(constants.ok), 50)
|
|
428
|
+
}
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
original_run_hooks(hook, c, params)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
conn.process_line(Buffer.from('QUIT\r\n'))
|
|
435
|
+
conn.process_line(Buffer.from('QUIT\r\n'))
|
|
436
|
+
|
|
437
|
+
await new Promise((resolve) => {
|
|
438
|
+
setTimeout(() => {
|
|
439
|
+
plugins.run_hooks = original_run_hooks
|
|
440
|
+
assert.equal(quit_hook_calls, 1, 'quit hook called exactly once')
|
|
441
|
+
resolve()
|
|
442
|
+
}, 100)
|
|
443
|
+
})
|
|
444
|
+
})
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
describe('queue responses', () => {
|
|
448
|
+
beforeEach(setUp)
|
|
449
|
+
|
|
450
|
+
const prepQueueTestConnection = () => {
|
|
451
|
+
const calls = { respond: [], reset: 0, disconnect: 0, queue_ok: 0, results: [] }
|
|
452
|
+
const plugins = require('../plugins')
|
|
453
|
+
const originalRunHooks = plugins.run_hooks
|
|
454
|
+
|
|
455
|
+
this.connection.transaction = {
|
|
456
|
+
uuid: 'txn-123',
|
|
457
|
+
msg_status: null,
|
|
458
|
+
results: {
|
|
459
|
+
add(_meta, payload) {
|
|
460
|
+
calls.results.push(payload)
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
this.connection.respond = (code, msg, cb) => {
|
|
466
|
+
calls.respond.push({ code, msg })
|
|
467
|
+
if (cb) cb()
|
|
468
|
+
}
|
|
469
|
+
this.connection.reset_transaction = (cb) => {
|
|
470
|
+
calls.reset++
|
|
471
|
+
this.connection.transaction = this.connection.transaction || {}
|
|
472
|
+
if (cb) cb()
|
|
473
|
+
}
|
|
474
|
+
this.connection.disconnect = () => {
|
|
475
|
+
calls.disconnect++
|
|
476
|
+
}
|
|
477
|
+
plugins.run_hooks = (hook) => {
|
|
478
|
+
if (hook === 'queue_ok') calls.queue_ok++
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
calls,
|
|
483
|
+
restore() {
|
|
484
|
+
plugins.run_hooks = originalRunHooks
|
|
485
|
+
},
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
it('queue_respond handles denydisconnect and marks message rejected', () => {
|
|
490
|
+
const harness = prepQueueTestConnection()
|
|
491
|
+
try {
|
|
492
|
+
this.connection.queue_respond(constants.denydisconnect)
|
|
493
|
+
assert.equal(harness.calls.respond[0].code, 550)
|
|
494
|
+
assert.equal(this.connection.msg_count.reject, 1)
|
|
495
|
+
assert.equal(this.connection.transaction.msg_status, 'rejected')
|
|
496
|
+
assert.equal(harness.calls.disconnect, 1)
|
|
497
|
+
assert.deepEqual(harness.calls.results[0], { fail: 'Message denied' })
|
|
498
|
+
} finally {
|
|
499
|
+
harness.restore()
|
|
500
|
+
}
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
it('queue_respond handles denysoft and resets transaction', () => {
|
|
504
|
+
const harness = prepQueueTestConnection()
|
|
505
|
+
try {
|
|
506
|
+
this.connection.queue_respond(constants.denysoft)
|
|
507
|
+
assert.equal(harness.calls.respond[0].code, 450)
|
|
508
|
+
assert.equal(this.connection.msg_count.tempfail, 1)
|
|
509
|
+
assert.equal(this.connection.transaction.msg_status, 'deferred')
|
|
510
|
+
assert.equal(harness.calls.reset, 1)
|
|
511
|
+
assert.deepEqual(harness.calls.results[0], {
|
|
512
|
+
fail: 'Message denied temporarily',
|
|
513
|
+
soft: true,
|
|
514
|
+
})
|
|
515
|
+
} finally {
|
|
516
|
+
harness.restore()
|
|
517
|
+
}
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('queue_respond handles denysoftdisconnect and disconnects', () => {
|
|
521
|
+
const harness = prepQueueTestConnection()
|
|
522
|
+
try {
|
|
523
|
+
this.connection.queue_respond(constants.denysoftdisconnect)
|
|
524
|
+
assert.equal(harness.calls.respond[0].code, 450)
|
|
525
|
+
assert.equal(this.connection.msg_count.tempfail, 1)
|
|
526
|
+
assert.equal(this.connection.transaction.msg_status, 'deferred')
|
|
527
|
+
assert.equal(harness.calls.disconnect, 1)
|
|
528
|
+
} finally {
|
|
529
|
+
harness.restore()
|
|
530
|
+
}
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it('queue_respond default path returns 451 and resets transaction', () => {
|
|
534
|
+
const harness = prepQueueTestConnection()
|
|
535
|
+
try {
|
|
536
|
+
this.connection.queue_respond(constants.cont)
|
|
537
|
+
assert.equal(harness.calls.respond[0].code, 451)
|
|
538
|
+
assert.equal(this.connection.msg_count.tempfail, 1)
|
|
539
|
+
assert.equal(this.connection.transaction.msg_status, 'deferred')
|
|
540
|
+
assert.equal(harness.calls.reset, 1)
|
|
541
|
+
} finally {
|
|
542
|
+
harness.restore()
|
|
543
|
+
}
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
it('queue_ok_respond accepts and resets transaction', () => {
|
|
547
|
+
const harness = prepQueueTestConnection()
|
|
548
|
+
try {
|
|
549
|
+
this.connection.queue_ok_respond(constants.ok, null, 'queued')
|
|
550
|
+
assert.equal(harness.calls.respond[0].code, 250)
|
|
551
|
+
assert.equal(this.connection.msg_count.accept, 1)
|
|
552
|
+
assert.equal(this.connection.transaction.msg_status, 'accepted')
|
|
553
|
+
assert.equal(harness.calls.reset, 1)
|
|
554
|
+
} finally {
|
|
555
|
+
harness.restore()
|
|
556
|
+
}
|
|
557
|
+
})
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
describe('smtp command/response branches', () => {
|
|
561
|
+
beforeEach(setUp)
|
|
562
|
+
|
|
563
|
+
it('rcpt_respond deny removes recipient and records reject', () => {
|
|
564
|
+
const plugins = require('../plugins')
|
|
565
|
+
const originalRunHooks = plugins.run_hooks
|
|
566
|
+
const rcpt = new Address('<to@example.com>')
|
|
567
|
+
const sender = new Address('<from@example.com>')
|
|
568
|
+
const actions = []
|
|
569
|
+
|
|
570
|
+
this.connection.transaction = {
|
|
571
|
+
rcpt_to: [rcpt],
|
|
572
|
+
mail_from: sender,
|
|
573
|
+
results: { push() {} },
|
|
574
|
+
}
|
|
575
|
+
this.connection.rcpt_incr = (_rcpt, action) => actions.push(action)
|
|
576
|
+
this.connection.respond = (_code, _msg, cb) => cb && cb()
|
|
577
|
+
plugins.run_hooks = () => {}
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
this.connection.rcpt_respond(constants.deny, 'no')
|
|
581
|
+
assert.equal(actions[0], 'reject')
|
|
582
|
+
assert.equal(this.connection.transaction.rcpt_to.length, 0)
|
|
583
|
+
} finally {
|
|
584
|
+
plugins.run_hooks = originalRunHooks
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
it('rcpt_respond ok runs rcpt_ok hook', () => {
|
|
589
|
+
const plugins = require('../plugins')
|
|
590
|
+
const originalRunHooks = plugins.run_hooks
|
|
591
|
+
const rcpt = new Address('<to@example.com>')
|
|
592
|
+
const sender = new Address('<from@example.com>')
|
|
593
|
+
const hooks = []
|
|
594
|
+
|
|
595
|
+
this.connection.transaction = {
|
|
596
|
+
rcpt_to: [rcpt],
|
|
597
|
+
mail_from: sender,
|
|
598
|
+
results: { push() {} },
|
|
599
|
+
}
|
|
600
|
+
this.connection.respond = (_code, _msg, cb) => cb && cb()
|
|
601
|
+
plugins.run_hooks = (hook) => hooks.push(hook)
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
this.connection.rcpt_respond(constants.ok, 'ok')
|
|
605
|
+
assert.equal(hooks.includes('rcpt_ok'), true)
|
|
606
|
+
assert.equal(this.connection.last_rcpt_msg, 'ok')
|
|
607
|
+
} finally {
|
|
608
|
+
plugins.run_hooks = originalRunHooks
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
it('cmd_proxy rejects when not allowed', () => {
|
|
613
|
+
let code
|
|
614
|
+
this.connection.proxy.allowed = false
|
|
615
|
+
this.connection.respond = (c) => {
|
|
616
|
+
code = c
|
|
617
|
+
}
|
|
618
|
+
this.connection.disconnect = () => {}
|
|
619
|
+
this.connection.cmd_proxy('TCP4 1.2.3.4 5.6.7.8 100 25')
|
|
620
|
+
assert.equal(code, 421)
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
it('cmd_proxy accepts valid TCP4 proxy line and runs connect_init', () => {
|
|
624
|
+
const plugins = require('../plugins')
|
|
625
|
+
const originalRunHooks = plugins.run_hooks
|
|
626
|
+
const hooks = []
|
|
627
|
+
this.connection.proxy.allowed = true
|
|
628
|
+
this.connection.remote.ip = '10.0.0.1'
|
|
629
|
+
this.connection.reset_transaction = (cb) => cb && cb()
|
|
630
|
+
this.connection.respond = () => {}
|
|
631
|
+
plugins.run_hooks = (hook) => hooks.push(hook)
|
|
632
|
+
|
|
633
|
+
try {
|
|
634
|
+
this.connection.cmd_proxy('TCP4 1.2.3.4 5.6.7.8 100 25')
|
|
635
|
+
assert.equal(this.connection.proxy.type, 'haproxy')
|
|
636
|
+
assert.equal(this.connection.remote.ip, '1.2.3.4')
|
|
637
|
+
assert.equal(this.connection.local.ip, '5.6.7.8')
|
|
638
|
+
assert.equal(hooks.includes('connect_init'), true)
|
|
639
|
+
} finally {
|
|
640
|
+
plugins.run_hooks = originalRunHooks
|
|
641
|
+
}
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
it('cmd_data validates argument/transaction/recipient preconditions', () => {
|
|
645
|
+
const responses = []
|
|
646
|
+
this.connection.respond = (code, msg) => {
|
|
647
|
+
responses.push([code, msg])
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
this.connection.cmd_data('unexpected')
|
|
651
|
+
this.connection.cmd_data()
|
|
652
|
+
this.connection.transaction = { rcpt_to: [] }
|
|
653
|
+
this.connection.cmd_data()
|
|
654
|
+
|
|
655
|
+
assert.equal(responses[0][0], 501)
|
|
656
|
+
assert.equal(responses[1][0], 503)
|
|
657
|
+
assert.equal(responses[2][0], 503)
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
it('cmd_mail strips control chars from invalid address logs', () => {
|
|
661
|
+
const notices = []
|
|
662
|
+
const responses = []
|
|
663
|
+
this.connection.hello.host = 'example.test'
|
|
664
|
+
this.connection.lognotice = (msg) => notices.push(msg)
|
|
665
|
+
this.connection.respond = (code, msg) => {
|
|
666
|
+
responses.push({ code, msg })
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
this.connection.cmd_mail('FROM:<mail\x00@example.com>')
|
|
670
|
+
|
|
671
|
+
assert.equal(responses[0].code, 501)
|
|
672
|
+
assert.match(responses[0].msg, /^Invalid MAIL FROM address /)
|
|
673
|
+
assert.equal(responses[0].msg.includes('\r'), false)
|
|
674
|
+
assert.equal(responses[0].msg.includes('\n'), false)
|
|
675
|
+
assert.equal(responses[0].msg.includes('\\u0000'), false)
|
|
676
|
+
assert.equal(notices[0], responses[0].msg)
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
it('cmd_rcpt strips control chars from invalid address logs', () => {
|
|
680
|
+
const notices = []
|
|
681
|
+
const responses = []
|
|
682
|
+
this.connection.transaction = { mail_from: new Address('<from@example.com>'), rcpt_to: [] }
|
|
683
|
+
this.connection.lognotice = (msg) => notices.push(msg)
|
|
684
|
+
this.connection.respond = (code, msg) => {
|
|
685
|
+
responses.push({ code, msg })
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
this.connection.cmd_rcpt('TO:<rcpt\x00@example.com>')
|
|
689
|
+
|
|
690
|
+
assert.equal(responses[0].code, 501)
|
|
691
|
+
assert.match(responses[0].msg, /^Invalid RCPT TO address /)
|
|
692
|
+
assert.equal(responses[0].msg.includes('\r'), false)
|
|
693
|
+
assert.equal(responses[0].msg.includes('\n'), false)
|
|
694
|
+
assert.equal(responses[0].msg.includes('\\u0000'), false)
|
|
695
|
+
assert.equal(notices[0], responses[0].msg)
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
it('cmd_mail rejects a postel-only address when main.postel is false', () => {
|
|
699
|
+
const responses = []
|
|
700
|
+
this.connection.hello.host = 'example.test'
|
|
701
|
+
this.connection.lognotice = () => {}
|
|
702
|
+
this.connection.respond = (code, msg) => responses.push({ code, msg })
|
|
703
|
+
|
|
704
|
+
this.connection.cmd_mail('FROM:<foo@[IPv6:bogus::xyz]>')
|
|
705
|
+
|
|
706
|
+
assert.equal(responses[0].code, 501)
|
|
707
|
+
assert.match(responses[0].msg, /^Invalid MAIL FROM address /)
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
it('cmd_mail accepts a postel-only address when main.postel is true', () => {
|
|
711
|
+
const responses = []
|
|
712
|
+
let started = false
|
|
713
|
+
this.connection.hello.host = 'example.test'
|
|
714
|
+
this.connection.respond = (code, msg) => responses.push({ code, msg })
|
|
715
|
+
this.connection.init_transaction = () => {
|
|
716
|
+
started = true
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
connection.cfg.main.postel = true
|
|
720
|
+
try {
|
|
721
|
+
this.connection.cmd_mail('FROM:<foo@[IPv6:bogus::xyz]>')
|
|
722
|
+
} finally {
|
|
723
|
+
connection.cfg.main.postel = false
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
assert.equal(started, true)
|
|
727
|
+
assert.equal(
|
|
728
|
+
responses.some((r) => r.code === 501),
|
|
729
|
+
false,
|
|
730
|
+
)
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
it('cmd_rcpt rejects a postel-only address when main.postel is false', () => {
|
|
734
|
+
const responses = []
|
|
735
|
+
this.connection.transaction = {
|
|
736
|
+
mail_from: new Address('<from@example.com>'),
|
|
737
|
+
rcpt_to: [],
|
|
738
|
+
}
|
|
739
|
+
this.connection.lognotice = () => {}
|
|
740
|
+
this.connection.respond = (code, msg) => responses.push({ code, msg })
|
|
741
|
+
|
|
742
|
+
this.connection.cmd_rcpt('TO:<foo@[IPv6:bogus::xyz]>')
|
|
743
|
+
|
|
744
|
+
assert.equal(responses[0].code, 501)
|
|
745
|
+
assert.match(responses[0].msg, /^Invalid RCPT TO address /)
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
it('cmd_rcpt accepts a postel-only address when main.postel is true', () => {
|
|
749
|
+
const plugins = require('../plugins')
|
|
750
|
+
const originalRunHooks = plugins.run_hooks
|
|
751
|
+
const responses = []
|
|
752
|
+
this.connection.transaction = {
|
|
753
|
+
mail_from: new Address('<from@example.com>'),
|
|
754
|
+
rcpt_to: [],
|
|
755
|
+
}
|
|
756
|
+
this.connection.respond = (code, msg) => responses.push({ code, msg })
|
|
757
|
+
plugins.run_hooks = () => {}
|
|
758
|
+
|
|
759
|
+
connection.cfg.main.postel = true
|
|
760
|
+
try {
|
|
761
|
+
this.connection.cmd_rcpt('TO:<foo@[IPv6:bogus::xyz]>')
|
|
762
|
+
} finally {
|
|
763
|
+
connection.cfg.main.postel = false
|
|
764
|
+
plugins.run_hooks = originalRunHooks
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
assert.equal(this.connection.transaction.rcpt_to.length, 1)
|
|
768
|
+
assert.equal(
|
|
769
|
+
responses.some((r) => r.code === 501),
|
|
770
|
+
false,
|
|
771
|
+
)
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
it('data_respond denysoftdisconnect disconnects and default enters DATA', () => {
|
|
775
|
+
const responses = []
|
|
776
|
+
let disconnected = 0
|
|
777
|
+
this.connection.transaction = { data_bytes: 5 }
|
|
778
|
+
this.connection.respond = (code, _msg, cb) => {
|
|
779
|
+
responses.push(code)
|
|
780
|
+
if (cb) cb()
|
|
781
|
+
}
|
|
782
|
+
this.connection.disconnect = () => {
|
|
783
|
+
disconnected++
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
this.connection.data_respond(constants.denysoftdisconnect, 'tmpfail')
|
|
787
|
+
this.connection.data_respond(constants.ok, 'ok')
|
|
788
|
+
|
|
789
|
+
assert.equal(responses[0], 451)
|
|
790
|
+
assert.equal(disconnected, 1)
|
|
791
|
+
assert.equal(responses[1], 354)
|
|
792
|
+
assert.equal(this.connection.state, constants.connection.state.DATA)
|
|
793
|
+
assert.equal(this.connection.transaction.data_bytes, 0)
|
|
794
|
+
})
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
describe('header injection', () => {
|
|
798
|
+
beforeEach(setUp)
|
|
799
|
+
|
|
800
|
+
it('cmd_helo rejects control chars in the host', () => {
|
|
801
|
+
const r = this.connection.cmd_helo('evil\rINJECTED')
|
|
802
|
+
assert.match(String(r), /^501/)
|
|
803
|
+
assert.equal(this.connection.hello.host, null)
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
it('cmd_ehlo rejects control chars in the host', () => {
|
|
807
|
+
const r = this.connection.cmd_ehlo('evil\r\nINJECTED')
|
|
808
|
+
assert.match(String(r), /^501/)
|
|
809
|
+
assert.equal(this.connection.hello.host, null)
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
it('auth_results strips CR/LF so it cannot inject a header', () => {
|
|
813
|
+
const out = this.connection.auth_results('auth=fail smtp.auth=evil\r\nInjected-Header: pwned')
|
|
814
|
+
assert.ok(!out.includes('\r\nInjected-Header:'))
|
|
815
|
+
// the only CRLF present is the legitimate folding (;\r\n\t)
|
|
816
|
+
assert.equal(out.replace(/;\r\n\t/g, '').includes('\r'), false)
|
|
817
|
+
assert.equal(out.replace(/;\r\n\t/g, '').includes('\n'), false)
|
|
818
|
+
})
|
|
819
|
+
})
|
|
820
|
+
})
|