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.
Files changed (254) hide show
  1. package/.githooks/pre-commit +41 -0
  2. package/.prettierignore +7 -0
  3. package/.qlty/.gitignore +7 -0
  4. package/.qlty/configs/.shellcheckrc +1 -0
  5. package/.qlty/qlty.toml +15 -0
  6. package/CHANGELOG.md +1898 -0
  7. package/CONTRIBUTORS.md +34 -0
  8. package/Dockerfile +50 -0
  9. package/LICENSE +22 -0
  10. package/Plugins.md +227 -0
  11. package/README.md +119 -4
  12. package/SECURITY.md +178 -0
  13. package/TODO +22 -0
  14. package/bin/haraka +593 -0
  15. package/bin/haraka_grep +32 -0
  16. package/config/aliases +2 -0
  17. package/config/auth_flat_file.ini +7 -0
  18. package/config/auth_vpopmaild.ini +9 -0
  19. package/config/connection.ini +79 -0
  20. package/config/delay_deny.ini +7 -0
  21. package/config/host_list +3 -0
  22. package/config/host_list_regex +6 -0
  23. package/config/http.ini +11 -0
  24. package/config/lmtp.ini +7 -0
  25. package/config/log.ini +11 -0
  26. package/config/outbound.bounce_message +18 -0
  27. package/config/outbound.bounce_message_html +36 -0
  28. package/config/outbound.bounce_message_image +106 -0
  29. package/config/outbound.ini +24 -0
  30. package/config/plugins +67 -0
  31. package/config/smtp.ini +37 -0
  32. package/config/smtp_bridge.ini +4 -0
  33. package/config/smtp_forward.ini +31 -0
  34. package/config/smtp_proxy.ini +27 -0
  35. package/config/tarpit.timeout +1 -0
  36. package/config/tls.ini +83 -0
  37. package/config/watch.ini +12 -0
  38. package/config/xclient.hosts +2 -0
  39. package/connection.js +1865 -0
  40. package/contrib/Haraka.cf +6 -0
  41. package/contrib/Haraka.pm +35 -0
  42. package/contrib/bad_smtp_server.pl +25 -0
  43. package/contrib/bsd-rc.d/haraka +63 -0
  44. package/contrib/debian-init.d/haraka +87 -0
  45. package/contrib/haraka.init +96 -0
  46. package/contrib/haraka.service +23 -0
  47. package/contrib/plugin2npm.sh +81 -0
  48. package/contrib/ubuntu-upstart/haraka.conf +27 -0
  49. package/docs/Body.md +1 -0
  50. package/docs/Config.md +1 -0
  51. package/docs/Connection.md +153 -0
  52. package/docs/CoreConfig.md +96 -0
  53. package/docs/CustomReturnCodes.md +3 -0
  54. package/docs/HAProxy.md +62 -0
  55. package/docs/Header.md +1 -0
  56. package/docs/Logging.md +129 -0
  57. package/docs/Outbound.md +210 -0
  58. package/docs/Plugins.md +372 -0
  59. package/docs/Results.md +7 -0
  60. package/docs/Transaction.md +135 -0
  61. package/docs/Tutorial.md +183 -0
  62. package/docs/deprecated/access.md +3 -0
  63. package/docs/deprecated/backscatterer.md +9 -0
  64. package/docs/deprecated/connect.rdns_access.md +53 -0
  65. package/docs/deprecated/data.headers.md +3 -0
  66. package/docs/deprecated/data.nomsgid.md +7 -0
  67. package/docs/deprecated/data.noreceived.md +11 -0
  68. package/docs/deprecated/data.rfc5322_header_checks.md +11 -0
  69. package/docs/deprecated/dkim_sign.md +97 -0
  70. package/docs/deprecated/dkim_verify.md +28 -0
  71. package/docs/deprecated/dnsbl.md +80 -0
  72. package/docs/deprecated/dnswl.md +73 -0
  73. package/docs/deprecated/lookup_rdns.strict.md +67 -0
  74. package/docs/deprecated/mail_from.access.md +52 -0
  75. package/docs/deprecated/mail_from.blocklist.md +18 -0
  76. package/docs/deprecated/mail_from.nobounces.md +8 -0
  77. package/docs/deprecated/rcpt_to.access.md +53 -0
  78. package/docs/deprecated/rcpt_to.blocklist.md +18 -0
  79. package/docs/deprecated/rcpt_to.routes.md +3 -0
  80. package/docs/deprecated/rdns.regexp.md +30 -0
  81. package/docs/plugins/aliases.md +3 -0
  82. package/docs/plugins/auth/auth_bridge.md +34 -0
  83. package/docs/plugins/auth/auth_ldap.md +4 -0
  84. package/docs/plugins/auth/auth_proxy.md +36 -0
  85. package/docs/plugins/auth/auth_vpopmaild.md +33 -0
  86. package/docs/plugins/auth/flat_file.md +40 -0
  87. package/docs/plugins/block_me.md +18 -0
  88. package/docs/plugins/data.signatures.md +11 -0
  89. package/docs/plugins/delay_deny.md +23 -0
  90. package/docs/plugins/max_unrecognized_commands.md +6 -0
  91. package/docs/plugins/prevent_credential_leaks.md +22 -0
  92. package/docs/plugins/process_title.md +42 -0
  93. package/docs/plugins/queue/deliver.md +3 -0
  94. package/docs/plugins/queue/discard.md +32 -0
  95. package/docs/plugins/queue/lmtp.md +24 -0
  96. package/docs/plugins/queue/qmail-queue.md +16 -0
  97. package/docs/plugins/queue/quarantine.md +87 -0
  98. package/docs/plugins/queue/smtp_bridge.md +32 -0
  99. package/docs/plugins/queue/smtp_forward.md +127 -0
  100. package/docs/plugins/queue/smtp_proxy.md +68 -0
  101. package/docs/plugins/queue/test.md +7 -0
  102. package/docs/plugins/rcpt_to.in_host_list.md +34 -0
  103. package/docs/plugins/rcpt_to.max_count.md +3 -0
  104. package/docs/plugins/record_envelope_addresses.md +20 -0
  105. package/docs/plugins/relay.md +3 -0
  106. package/docs/plugins/reseed_rng.md +16 -0
  107. package/docs/plugins/status.md +41 -0
  108. package/docs/plugins/tarpit.md +50 -0
  109. package/docs/plugins/tls.md +235 -0
  110. package/docs/plugins/toobusy.md +27 -0
  111. package/docs/plugins/xclient.md +10 -0
  112. package/docs/tutorials/Migrating_from_v1_to_v2.md +96 -0
  113. package/docs/tutorials/SettingUpOutbound.md +62 -0
  114. package/eslint.config.mjs +2 -0
  115. package/haraka.js +74 -0
  116. package/haraka.sh +2 -0
  117. package/http/html/404.html +58 -0
  118. package/http/html/index.html +47 -0
  119. package/http/package.json +21 -0
  120. package/line_socket.js +24 -0
  121. package/logger.js +322 -0
  122. package/outbound/client_pool.js +59 -0
  123. package/outbound/config.js +134 -0
  124. package/outbound/hmail.js +1504 -0
  125. package/outbound/index.js +349 -0
  126. package/outbound/qfile.js +93 -0
  127. package/outbound/queue.js +399 -0
  128. package/outbound/tls.js +85 -0
  129. package/outbound/todo.js +17 -0
  130. package/package.json +100 -4
  131. package/plugins/.eslintrc.yaml +3 -0
  132. package/plugins/auth/auth_base.js +261 -0
  133. package/plugins/auth/auth_bridge.js +20 -0
  134. package/plugins/auth/auth_proxy.js +227 -0
  135. package/plugins/auth/auth_vpopmaild.js +162 -0
  136. package/plugins/auth/flat_file.js +44 -0
  137. package/plugins/block_me.js +88 -0
  138. package/plugins/data.signatures.js +30 -0
  139. package/plugins/delay_deny.js +153 -0
  140. package/plugins/prevent_credential_leaks.js +61 -0
  141. package/plugins/process_title.js +197 -0
  142. package/plugins/profile.js +11 -0
  143. package/plugins/queue/deliver.js +12 -0
  144. package/plugins/queue/discard.js +27 -0
  145. package/plugins/queue/lmtp.js +45 -0
  146. package/plugins/queue/qmail-queue.js +93 -0
  147. package/plugins/queue/quarantine.js +133 -0
  148. package/plugins/queue/smtp_bridge.js +45 -0
  149. package/plugins/queue/smtp_forward.js +371 -0
  150. package/plugins/queue/smtp_proxy.js +142 -0
  151. package/plugins/queue/test.js +15 -0
  152. package/plugins/rcpt_to.host_list_base.js +65 -0
  153. package/plugins/rcpt_to.in_host_list.js +56 -0
  154. package/plugins/record_envelope_addresses.js +17 -0
  155. package/plugins/reseed_rng.js +7 -0
  156. package/plugins/status.js +274 -0
  157. package/plugins/tarpit.js +45 -0
  158. package/plugins/tls.js +164 -0
  159. package/plugins/toobusy.js +47 -0
  160. package/plugins/xclient.js +124 -0
  161. package/plugins.js +605 -0
  162. package/run_tests +11 -0
  163. package/server.js +827 -0
  164. package/smtp_client.js +504 -0
  165. package/test/.eslintrc.yaml +11 -0
  166. package/test/config/auth_flat_file.ini +5 -0
  167. package/test/config/block_me.recipient +1 -0
  168. package/test/config/block_me.senders +1 -0
  169. package/test/config/dhparams.pem +8 -0
  170. package/test/config/host_list +2 -0
  171. package/test/config/outbound_tls_cert.pem +1 -0
  172. package/test/config/outbound_tls_key.pem +1 -0
  173. package/test/config/plugins +7 -0
  174. package/test/config/smtp.ini +11 -0
  175. package/test/config/smtp_forward.ini +30 -0
  176. package/test/config/tls/example.com/_.example.com.key +28 -0
  177. package/test/config/tls/example.com/example.com.crt +25 -0
  178. package/test/config/tls/haraka.local.pem +51 -0
  179. package/test/config/tls.ini +45 -0
  180. package/test/config/tls_cert.pem +21 -0
  181. package/test/config/tls_key.pem +28 -0
  182. package/test/connection.js +820 -0
  183. package/test/fixtures/haproxy_allowed/config/connection.ini +3 -0
  184. package/test/fixtures/haproxy_disabled/config/connection.ini +3 -0
  185. package/test/fixtures/haproxy_untrusted/config/connection.ini +3 -0
  186. package/test/fixtures/line_socket.js +21 -0
  187. package/test/fixtures/todo_qfile.txt +0 -0
  188. package/test/fixtures/util_hmailitem.js +156 -0
  189. package/test/installation/config/test-plugin-flat +1 -0
  190. package/test/installation/config/test-plugin.ini +10 -0
  191. package/test/installation/config/tls.ini +1 -0
  192. package/test/installation/node_modules/load_first/index.js +5 -0
  193. package/test/installation/node_modules/load_first/package.json +11 -0
  194. package/test/installation/node_modules/test-plugin/config/test-plugin-flat +1 -0
  195. package/test/installation/node_modules/test-plugin/config/test-plugin.ini +9 -0
  196. package/test/installation/node_modules/test-plugin/package.json +5 -0
  197. package/test/installation/node_modules/test-plugin/test-plugin.js +5 -0
  198. package/test/installation/plugins/base_plugin.js +3 -0
  199. package/test/installation/plugins/folder_plugin/index.js +3 -0
  200. package/test/installation/plugins/folder_plugin/package.json +11 -0
  201. package/test/installation/plugins/inherits.js +7 -0
  202. package/test/installation/plugins/load_first.js +3 -0
  203. package/test/installation/plugins/plugin.js +1 -0
  204. package/test/installation/plugins/tls.js +3 -0
  205. package/test/logger.js +217 -0
  206. package/test/loud/config/dhparams.pem +0 -0
  207. package/test/loud/config/tls/goobered.pem +45 -0
  208. package/test/loud/config/tls.ini +43 -0
  209. package/test/mail_specimen/base64-root-part.txt +23 -0
  210. package/test/mail_specimen/varied-fold-lengths-preserve-data.txt +283 -0
  211. package/test/outbound/bounce_net_errors.js +133 -0
  212. package/test/outbound/bounce_rfc3464.js +226 -0
  213. package/test/outbound/hmail.js +210 -0
  214. package/test/outbound/index.js +385 -0
  215. package/test/outbound/qfile.js +124 -0
  216. package/test/outbound/queue.js +325 -0
  217. package/test/plugins/auth/auth_base.js +620 -0
  218. package/test/plugins/auth/auth_bridge.js +80 -0
  219. package/test/plugins/auth/auth_vpopmaild.js +81 -0
  220. package/test/plugins/auth/flat_file.js +123 -0
  221. package/test/plugins/block_me.js +141 -0
  222. package/test/plugins/data.signatures.js +111 -0
  223. package/test/plugins/delay_deny.js +262 -0
  224. package/test/plugins/prevent_credential_leaks.js +174 -0
  225. package/test/plugins/process_title.js +141 -0
  226. package/test/plugins/queue/deliver.js +98 -0
  227. package/test/plugins/queue/discard.js +78 -0
  228. package/test/plugins/queue/lmtp.js +137 -0
  229. package/test/plugins/queue/qmail-queue.js +98 -0
  230. package/test/plugins/queue/quarantine.js +80 -0
  231. package/test/plugins/queue/smtp_bridge.js +152 -0
  232. package/test/plugins/queue/smtp_forward.js +1023 -0
  233. package/test/plugins/queue/smtp_proxy.js +138 -0
  234. package/test/plugins/rcpt_to.host_list_base.js +102 -0
  235. package/test/plugins/rcpt_to.in_host_list.js +186 -0
  236. package/test/plugins/record_envelope_addresses.js +66 -0
  237. package/test/plugins/reseed_rng.js +34 -0
  238. package/test/plugins/status.js +207 -0
  239. package/test/plugins/tarpit.js +90 -0
  240. package/test/plugins/tls.js +86 -0
  241. package/test/plugins/toobusy.js +198 -0
  242. package/test/plugins/xclient.js +119 -0
  243. package/test/plugins.js +230 -0
  244. package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_fixed +0 -0
  245. package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka +0 -0
  246. package/test/queue/1508269674999_1508269674999_0_34002_socVUF_1_haraka +0 -0
  247. package/test/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka +0 -0
  248. package/test/queue/zero-length +0 -0
  249. package/test/server.js +1012 -0
  250. package/test/smtp_client.js +1303 -0
  251. package/test/tls_socket.js +321 -0
  252. package/test/transaction.js +554 -0
  253. package/tls_socket.js +771 -0
  254. 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
+ })