haraka 0.0.33 → 3.3.0

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 (309) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/.githooks/pre-commit +41 -0
  3. package/.prettierignore +6 -0
  4. package/.qlty/.gitignore +7 -0
  5. package/.qlty/configs/.shellcheckrc +1 -0
  6. package/.qlty/qlty.toml +15 -0
  7. package/CHANGELOG.md +1894 -0
  8. package/CLAUDE.md +40 -0
  9. package/CONTRIBUTORS.md +34 -0
  10. package/Dockerfile +50 -0
  11. package/GEMINI.md +38 -0
  12. package/LICENSE +22 -0
  13. package/Plugins.md +227 -0
  14. package/README.md +119 -4
  15. package/SECURITY.md +178 -0
  16. package/TODO +22 -0
  17. package/address.js +53 -0
  18. package/bin/haraka +593 -0
  19. package/bin/haraka_grep +32 -0
  20. package/config/aliases +2 -0
  21. package/config/auth_flat_file.ini +7 -0
  22. package/config/auth_vpopmaild.ini +9 -0
  23. package/config/connection.ini +79 -0
  24. package/config/delay_deny.ini +7 -0
  25. package/config/dhparams.pem +8 -0
  26. package/config/host_list +3 -0
  27. package/config/host_list_regex +6 -0
  28. package/config/http.ini +11 -0
  29. package/config/lmtp.ini +7 -0
  30. package/config/log.ini +11 -0
  31. package/config/me +1 -0
  32. package/config/outbound.bounce_message +18 -0
  33. package/config/outbound.bounce_message_html +36 -0
  34. package/config/outbound.bounce_message_image +106 -0
  35. package/config/outbound.ini +24 -0
  36. package/config/plugins +67 -0
  37. package/config/smtp.ini +37 -0
  38. package/config/smtp_bridge.ini +4 -0
  39. package/config/smtp_forward.ini +31 -0
  40. package/config/smtp_proxy.ini +27 -0
  41. package/config/tarpit.timeout +1 -0
  42. package/config/tls.ini +83 -0
  43. package/config/tls_cert.pem +23 -0
  44. package/config/tls_key.pem +28 -0
  45. package/config/watch.ini +12 -0
  46. package/config/xclient.hosts +2 -0
  47. package/connection.js +1863 -0
  48. package/contrib/Haraka.cf +6 -0
  49. package/contrib/Haraka.pm +35 -0
  50. package/contrib/bad_smtp_server.pl +25 -0
  51. package/contrib/bsd-rc.d/haraka +61 -0
  52. package/contrib/debian-init.d/haraka +87 -0
  53. package/contrib/haraka.init +96 -0
  54. package/contrib/haraka.service +23 -0
  55. package/contrib/plugin2npm.sh +81 -0
  56. package/contrib/ubuntu-upstart/haraka.conf +27 -0
  57. package/coverage/coverage-final.json +2 -0
  58. package/coverage/coverage-summary.json +33 -0
  59. package/coverage/tmp/coverage-79131-1779241025146-0.json +1 -0
  60. package/coverage/tmp/coverage-79132-1779240999690-0.json +1 -0
  61. package/coverage/tmp/coverage-79172-1779241000095-0.json +1 -0
  62. package/coverage/tmp/coverage-79210-1779241000156-0.json +1 -0
  63. package/coverage/tmp/coverage-79211-1779241000209-0.json +1 -0
  64. package/coverage/tmp/coverage-79212-1779241000266-0.json +1 -0
  65. package/coverage/tmp/coverage-79213-1779241000441-0.json +1 -0
  66. package/coverage/tmp/coverage-79214-1779241000626-0.json +1 -0
  67. package/coverage/tmp/coverage-79215-1779241000795-0.json +1 -0
  68. package/coverage/tmp/coverage-79216-1779241000965-0.json +1 -0
  69. package/coverage/tmp/coverage-79218-1779241001013-0.json +1 -0
  70. package/coverage/tmp/coverage-79219-1779241001179-0.json +1 -0
  71. package/coverage/tmp/coverage-79220-1779241006249-0.json +1 -0
  72. package/coverage/tmp/coverage-79227-1779241011453-0.json +1 -0
  73. package/coverage/tmp/coverage-79229-1779241011537-0.json +1 -0
  74. package/coverage/tmp/coverage-79230-1779241011647-0.json +1 -0
  75. package/coverage/tmp/coverage-79231-1779241011765-0.json +1 -0
  76. package/coverage/tmp/coverage-79232-1779241011841-0.json +1 -0
  77. package/coverage/tmp/coverage-79233-1779241011909-0.json +1 -0
  78. package/coverage/tmp/coverage-79234-1779241011984-0.json +1 -0
  79. package/coverage/tmp/coverage-79235-1779241012055-0.json +1 -0
  80. package/coverage/tmp/coverage-79236-1779241012230-0.json +1 -0
  81. package/coverage/tmp/coverage-79237-1779241012300-0.json +1 -0
  82. package/coverage/tmp/coverage-79238-1779241012368-0.json +1 -0
  83. package/coverage/tmp/coverage-79239-1779241012438-0.json +1 -0
  84. package/coverage/tmp/coverage-79240-1779241012511-0.json +1 -0
  85. package/coverage/tmp/coverage-79241-1779241012582-0.json +1 -0
  86. package/coverage/tmp/coverage-79242-1779241012652-0.json +1 -0
  87. package/coverage/tmp/coverage-79243-1779241012814-0.json +1 -0
  88. package/coverage/tmp/coverage-79244-1779241012931-0.json +1 -0
  89. package/coverage/tmp/coverage-79245-1779241013007-0.json +1 -0
  90. package/coverage/tmp/coverage-79246-1779241013106-0.json +1 -0
  91. package/coverage/tmp/coverage-79247-1779241013178-0.json +1 -0
  92. package/coverage/tmp/coverage-79248-1779241013244-0.json +1 -0
  93. package/coverage/tmp/coverage-79249-1779241013409-0.json +1 -0
  94. package/coverage/tmp/coverage-79250-1779241013697-0.json +1 -0
  95. package/coverage/tmp/coverage-79251-1779241013847-0.json +1 -0
  96. package/coverage/tmp/coverage-79252-1779241014288-0.json +1 -0
  97. package/coverage/tmp/coverage-79253-1779241014378-0.json +1 -0
  98. package/coverage/tmp/coverage-79254-1779241014428-0.json +1 -0
  99. package/coverage/tmp/coverage-79255-1779241021774-0.json +1 -0
  100. package/coverage/tmp/coverage-80382-1779241021949-0.json +1 -0
  101. package/coverage/tmp/coverage-80383-1779241025019-0.json +1 -0
  102. package/coverage/tmp/coverage-80384-1779241025133-0.json +1 -0
  103. package/docs/Body.md +1 -0
  104. package/docs/Config.md +1 -0
  105. package/docs/Connection.md +153 -0
  106. package/docs/CoreConfig.md +96 -0
  107. package/docs/CustomReturnCodes.md +3 -0
  108. package/docs/HAProxy.md +62 -0
  109. package/docs/Header.md +1 -0
  110. package/docs/Logging.md +129 -0
  111. package/docs/Outbound.md +210 -0
  112. package/docs/Plugins.md +372 -0
  113. package/docs/Results.md +7 -0
  114. package/docs/Transaction.md +135 -0
  115. package/docs/Tutorial.md +183 -0
  116. package/docs/deprecated/access.md +3 -0
  117. package/docs/deprecated/backscatterer.md +9 -0
  118. package/docs/deprecated/connect.rdns_access.md +53 -0
  119. package/docs/deprecated/data.headers.md +3 -0
  120. package/docs/deprecated/data.nomsgid.md +7 -0
  121. package/docs/deprecated/data.noreceived.md +11 -0
  122. package/docs/deprecated/data.rfc5322_header_checks.md +11 -0
  123. package/docs/deprecated/dkim_sign.md +97 -0
  124. package/docs/deprecated/dkim_verify.md +28 -0
  125. package/docs/deprecated/dnsbl.md +80 -0
  126. package/docs/deprecated/dnswl.md +73 -0
  127. package/docs/deprecated/lookup_rdns.strict.md +67 -0
  128. package/docs/deprecated/mail_from.access.md +52 -0
  129. package/docs/deprecated/mail_from.blocklist.md +18 -0
  130. package/docs/deprecated/mail_from.nobounces.md +8 -0
  131. package/docs/deprecated/rcpt_to.access.md +53 -0
  132. package/docs/deprecated/rcpt_to.blocklist.md +18 -0
  133. package/docs/deprecated/rcpt_to.routes.md +3 -0
  134. package/docs/deprecated/rdns.regexp.md +30 -0
  135. package/docs/plugins/aliases.md +3 -0
  136. package/docs/plugins/auth/auth_bridge.md +34 -0
  137. package/docs/plugins/auth/auth_ldap.md +4 -0
  138. package/docs/plugins/auth/auth_proxy.md +36 -0
  139. package/docs/plugins/auth/auth_vpopmaild.md +33 -0
  140. package/docs/plugins/auth/flat_file.md +40 -0
  141. package/docs/plugins/block_me.md +18 -0
  142. package/docs/plugins/data.signatures.md +11 -0
  143. package/docs/plugins/delay_deny.md +23 -0
  144. package/docs/plugins/max_unrecognized_commands.md +6 -0
  145. package/docs/plugins/prevent_credential_leaks.md +22 -0
  146. package/docs/plugins/process_title.md +42 -0
  147. package/docs/plugins/queue/deliver.md +3 -0
  148. package/docs/plugins/queue/discard.md +32 -0
  149. package/docs/plugins/queue/lmtp.md +24 -0
  150. package/docs/plugins/queue/qmail-queue.md +16 -0
  151. package/docs/plugins/queue/quarantine.md +87 -0
  152. package/docs/plugins/queue/smtp_bridge.md +32 -0
  153. package/docs/plugins/queue/smtp_forward.md +127 -0
  154. package/docs/plugins/queue/smtp_proxy.md +68 -0
  155. package/docs/plugins/queue/test.md +7 -0
  156. package/docs/plugins/rcpt_to.in_host_list.md +34 -0
  157. package/docs/plugins/rcpt_to.max_count.md +3 -0
  158. package/docs/plugins/record_envelope_addresses.md +20 -0
  159. package/docs/plugins/relay.md +3 -0
  160. package/docs/plugins/reseed_rng.md +16 -0
  161. package/docs/plugins/status.md +41 -0
  162. package/docs/plugins/tarpit.md +50 -0
  163. package/docs/plugins/tls.md +235 -0
  164. package/docs/plugins/toobusy.md +27 -0
  165. package/docs/plugins/xclient.md +10 -0
  166. package/docs/tutorials/Migrating_from_v1_to_v2.md +96 -0
  167. package/docs/tutorials/SettingUpOutbound.md +62 -0
  168. package/eslint.config.mjs +2 -0
  169. package/haraka.js +74 -0
  170. package/haraka.sh +2 -0
  171. package/http/html/404.html +58 -0
  172. package/http/html/index.html +47 -0
  173. package/http/package.json +21 -0
  174. package/line_socket.js +24 -0
  175. package/logger.js +322 -0
  176. package/outbound/client_pool.js +59 -0
  177. package/outbound/config.js +134 -0
  178. package/outbound/hmail.js +1504 -0
  179. package/outbound/index.js +349 -0
  180. package/outbound/qfile.js +93 -0
  181. package/outbound/queue.js +399 -0
  182. package/outbound/tls.js +85 -0
  183. package/outbound/todo.js +17 -0
  184. package/package.json +99 -4
  185. package/plugins/.eslintrc.yaml +3 -0
  186. package/plugins/auth/auth_base.js +261 -0
  187. package/plugins/auth/auth_bridge.js +20 -0
  188. package/plugins/auth/auth_proxy.js +227 -0
  189. package/plugins/auth/auth_vpopmaild.js +162 -0
  190. package/plugins/auth/flat_file.js +44 -0
  191. package/plugins/block_me.js +88 -0
  192. package/plugins/data.signatures.js +30 -0
  193. package/plugins/delay_deny.js +153 -0
  194. package/plugins/prevent_credential_leaks.js +61 -0
  195. package/plugins/process_title.js +197 -0
  196. package/plugins/profile.js +11 -0
  197. package/plugins/queue/deliver.js +12 -0
  198. package/plugins/queue/discard.js +27 -0
  199. package/plugins/queue/lmtp.js +45 -0
  200. package/plugins/queue/qmail-queue.js +93 -0
  201. package/plugins/queue/quarantine.js +133 -0
  202. package/plugins/queue/smtp_bridge.js +45 -0
  203. package/plugins/queue/smtp_forward.js +371 -0
  204. package/plugins/queue/smtp_proxy.js +142 -0
  205. package/plugins/queue/test.js +15 -0
  206. package/plugins/rcpt_to.host_list_base.js +65 -0
  207. package/plugins/rcpt_to.in_host_list.js +56 -0
  208. package/plugins/record_envelope_addresses.js +17 -0
  209. package/plugins/reseed_rng.js +7 -0
  210. package/plugins/status.js +274 -0
  211. package/plugins/tarpit.js +45 -0
  212. package/plugins/tls.js +164 -0
  213. package/plugins/toobusy.js +47 -0
  214. package/plugins/xclient.js +124 -0
  215. package/plugins.js +604 -0
  216. package/queue/1772642154987_1775581346001_4_82235_TGwgfd_2_mattbook-m3.home.simerson.net +0 -0
  217. package/run_tests +11 -0
  218. package/server.js +827 -0
  219. package/smtp_client.js +504 -0
  220. package/test/.eslintrc.yaml +11 -0
  221. package/test/config/auth_flat_file.ini +5 -0
  222. package/test/config/block_me.recipient +1 -0
  223. package/test/config/block_me.senders +1 -0
  224. package/test/config/dhparams.pem +8 -0
  225. package/test/config/host_list +2 -0
  226. package/test/config/outbound_tls_cert.pem +1 -0
  227. package/test/config/outbound_tls_key.pem +1 -0
  228. package/test/config/plugins +7 -0
  229. package/test/config/smtp.ini +11 -0
  230. package/test/config/smtp_forward.ini +30 -0
  231. package/test/config/tls/example.com/_.example.com.key +28 -0
  232. package/test/config/tls/example.com/example.com.crt +25 -0
  233. package/test/config/tls/haraka.local.pem +51 -0
  234. package/test/config/tls.ini +45 -0
  235. package/test/config/tls_cert.pem +21 -0
  236. package/test/config/tls_key.pem +28 -0
  237. package/test/connection.js +817 -0
  238. package/test/fixtures/haproxy_allowed/config/connection.ini +3 -0
  239. package/test/fixtures/haproxy_disabled/config/connection.ini +3 -0
  240. package/test/fixtures/haproxy_untrusted/config/connection.ini +3 -0
  241. package/test/fixtures/line_socket.js +21 -0
  242. package/test/fixtures/todo_qfile.txt +0 -0
  243. package/test/fixtures/util_hmailitem.js +156 -0
  244. package/test/installation/config/test-plugin-flat +1 -0
  245. package/test/installation/config/test-plugin.ini +10 -0
  246. package/test/installation/config/tls.ini +1 -0
  247. package/test/installation/node_modules/load_first/index.js +5 -0
  248. package/test/installation/node_modules/load_first/package.json +11 -0
  249. package/test/installation/node_modules/test-plugin/config/test-plugin-flat +1 -0
  250. package/test/installation/node_modules/test-plugin/config/test-plugin.ini +9 -0
  251. package/test/installation/node_modules/test-plugin/package.json +5 -0
  252. package/test/installation/node_modules/test-plugin/test-plugin.js +5 -0
  253. package/test/installation/plugins/base_plugin.js +3 -0
  254. package/test/installation/plugins/folder_plugin/index.js +3 -0
  255. package/test/installation/plugins/folder_plugin/package.json +11 -0
  256. package/test/installation/plugins/inherits.js +7 -0
  257. package/test/installation/plugins/load_first.js +3 -0
  258. package/test/installation/plugins/plugin.js +1 -0
  259. package/test/installation/plugins/tls.js +3 -0
  260. package/test/logger.js +217 -0
  261. package/test/loud/config/dhparams.pem +0 -0
  262. package/test/loud/config/tls/goobered.pem +45 -0
  263. package/test/loud/config/tls.ini +43 -0
  264. package/test/mail_specimen/base64-root-part.txt +23 -0
  265. package/test/mail_specimen/varied-fold-lengths-preserve-data.txt +283 -0
  266. package/test/outbound/bounce_net_errors.js +133 -0
  267. package/test/outbound/bounce_rfc3464.js +226 -0
  268. package/test/outbound/hmail.js +210 -0
  269. package/test/outbound/index.js +385 -0
  270. package/test/outbound/qfile.js +124 -0
  271. package/test/outbound/queue.js +325 -0
  272. package/test/plugins/auth/auth_base.js +620 -0
  273. package/test/plugins/auth/auth_bridge.js +80 -0
  274. package/test/plugins/auth/auth_vpopmaild.js +81 -0
  275. package/test/plugins/auth/flat_file.js +123 -0
  276. package/test/plugins/block_me.js +141 -0
  277. package/test/plugins/data.signatures.js +111 -0
  278. package/test/plugins/delay_deny.js +262 -0
  279. package/test/plugins/prevent_credential_leaks.js +174 -0
  280. package/test/plugins/process_title.js +141 -0
  281. package/test/plugins/queue/deliver.js +98 -0
  282. package/test/plugins/queue/discard.js +78 -0
  283. package/test/plugins/queue/lmtp.js +137 -0
  284. package/test/plugins/queue/qmail-queue.js +98 -0
  285. package/test/plugins/queue/quarantine.js +80 -0
  286. package/test/plugins/queue/smtp_bridge.js +152 -0
  287. package/test/plugins/queue/smtp_forward.js +1023 -0
  288. package/test/plugins/queue/smtp_proxy.js +138 -0
  289. package/test/plugins/rcpt_to.host_list_base.js +102 -0
  290. package/test/plugins/rcpt_to.in_host_list.js +186 -0
  291. package/test/plugins/record_envelope_addresses.js +66 -0
  292. package/test/plugins/reseed_rng.js +34 -0
  293. package/test/plugins/status.js +207 -0
  294. package/test/plugins/tarpit.js +90 -0
  295. package/test/plugins/tls.js +86 -0
  296. package/test/plugins/toobusy.js +21 -0
  297. package/test/plugins/xclient.js +119 -0
  298. package/test/plugins.js +230 -0
  299. package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_fixed +0 -0
  300. package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka +0 -0
  301. package/test/queue/1508269674999_1508269674999_0_34002_socVUF_1_haraka +0 -0
  302. package/test/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka +0 -0
  303. package/test/queue/zero-length +0 -0
  304. package/test/server.js +1012 -0
  305. package/test/smtp_client.js +1303 -0
  306. package/test/tls_socket.js +321 -0
  307. package/test/transaction.js +554 -0
  308. package/tls_socket.js +771 -0
  309. package/transaction.js +267 -0
@@ -0,0 +1,210 @@
1
+ 'use strict'
2
+
3
+ const { describe, it, before, beforeEach, afterEach } = require('node:test')
4
+ const assert = require('node:assert')
5
+ const { EventEmitter } = require('node:events')
6
+ const fs = require('node:fs')
7
+ const path = require('node:path')
8
+
9
+ // Load outbound/index FIRST to avoid the circular-dependency boot-order issue.
10
+ const outbound = require('../../outbound')
11
+ const Hmail = outbound.HMailItem
12
+ const client_pool = require('../../outbound/client_pool')
13
+
14
+ // ── Helpers ───────────────────────────────────────────────────────────────────
15
+
16
+ const onEvent = (emitter, event) => new Promise((resolve) => emitter.once(event, resolve))
17
+
18
+ // ── Tests ─────────────────────────────────────────────────────────────────────
19
+
20
+ describe('outbound/hmail', () => {
21
+ let hmail
22
+
23
+ beforeEach(() => {
24
+ hmail = new Hmail(
25
+ '1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka',
26
+ 'test/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka',
27
+ {},
28
+ )
29
+ })
30
+
31
+ describe('socket error/timeout handler robustness (#3388)', () => {
32
+ const mx = { using_lmtp: false, port: 25, exchange: 'mx.example.com', bind: null, bind_helo: 'test' }
33
+ let origRelease
34
+
35
+ function makeSocket() {
36
+ const s = new EventEmitter()
37
+ s.name = 'mock'
38
+ s.writable = true
39
+ s.write = () => {}
40
+ s.destroy = () => {}
41
+ return s
42
+ }
43
+
44
+ beforeEach(() => {
45
+ origRelease = client_pool.release_client
46
+ client_pool.release_client = () => {}
47
+ hmail.todo = { rcpt_to: [] }
48
+ hmail.try_deliver = () => {}
49
+ hmail.logerror = () => {}
50
+ })
51
+
52
+ afterEach(() => {
53
+ client_pool.release_client = origRelease
54
+ })
55
+
56
+ it('error then timeout does not throw ERR_UNHANDLED_ERROR', () => {
57
+ const socket = makeSocket()
58
+ hmail.try_deliver_host_on_socket(mx, '1.2.3.4', 25, socket)
59
+ socket.emit('error', new Error('connection refused'))
60
+ assert.doesNotThrow(() => socket.emit('timeout'), 'timeout after error must not crash')
61
+ })
62
+
63
+ it('timeout then error does not throw ERR_UNHANDLED_ERROR', () => {
64
+ const socket = makeSocket()
65
+ hmail.try_deliver_host_on_socket(mx, '1.2.3.4', 25, socket)
66
+ socket.emit('timeout')
67
+ assert.doesNotThrow(
68
+ () => socket.emit('error', new Error('late error')),
69
+ 'error after timeout must not crash',
70
+ )
71
+ })
72
+
73
+ it('multiple timeouts do not throw ERR_UNHANDLED_ERROR', () => {
74
+ const socket = makeSocket()
75
+ hmail.try_deliver_host_on_socket(mx, '1.2.3.4', 25, socket)
76
+ socket.emit('timeout')
77
+ assert.doesNotThrow(() => socket.emit('timeout'), 'second timeout must not crash')
78
+ })
79
+ })
80
+
81
+ it('sort_mx orders by priority ascending', () => {
82
+ const sorted = hmail.sort_mx([
83
+ { exchange: 'mx2.example.com', priority: 5 },
84
+ { exchange: 'mx1.example.com', priority: 6 },
85
+ ])
86
+ assert.equal(sorted[0].exchange, 'mx2.example.com')
87
+ })
88
+
89
+ it('sort_mx shuffles equal-priority entries', () => {
90
+ const sorted = hmail.sort_mx([
91
+ { exchange: 'mx2.example.com', priority: 5 },
92
+ { exchange: 'mx1.example.com', priority: 6 },
93
+ { exchange: 'mx3.example.com', priority: 6 },
94
+ ])
95
+ assert.equal(sorted[0].exchange, 'mx2.example.com')
96
+ assert.ok(['mx1.example.com', 'mx3.example.com'].includes(sorted[1].exchange))
97
+ })
98
+
99
+ it('get_force_tls matches by IP and domain', () => {
100
+ hmail.todo = { domain: 'miss.example.com' }
101
+ hmail.obtls.cfg = { force_tls_hosts: ['1.2.3.4', 'hit.example.com'] }
102
+ assert.equal(hmail.get_force_tls({ exchange: '1.2.3.4' }), true)
103
+ assert.equal(hmail.get_force_tls({ exchange: '1.2.3.5' }), false)
104
+ hmail.todo = { domain: 'hit.example.com' }
105
+ assert.equal(hmail.get_force_tls({ exchange: '1.2.3.5' }), true)
106
+ })
107
+ })
108
+
109
+ const TOOLONG_FIXTURE = 'test/queue/1509000000000_1509000000000_0_99999_ToLong_1_haraka'
110
+
111
+ const makeToolongFixture = () => {
112
+ const buf = Buffer.alloc(50)
113
+ buf.writeUInt32BE(9999, 0) // declares 9999 bytes but file has only 46 after the header
114
+ buf.write('{"domain":"example.com"', 4)
115
+ fs.writeFileSync(TOOLONG_FIXTURE, buf)
116
+ }
117
+
118
+ describe('outbound/hmail.HMailItem — queue file loading', () => {
119
+ before(makeToolongFixture)
120
+
121
+ it('loads a valid queue file', async () => {
122
+ const h = new Hmail(
123
+ '1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka',
124
+ 'test/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka',
125
+ {},
126
+ )
127
+ await onEvent(h, 'ready')
128
+ assert.ok(h)
129
+ })
130
+
131
+ it('loads a TODO with multibyte chars without error', async () => {
132
+ const h = new Hmail('1507509981169_1507509981169_0_61403_e0Y0Ym_1_qfile', 'test/fixtures/todo_qfile.txt', {})
133
+ await onEvent(h, 'ready')
134
+ assert.ok(h)
135
+ })
136
+
137
+ it('emits error on too-short declared TODO length', async () => {
138
+ const h = new Hmail(
139
+ '1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka',
140
+ 'test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka',
141
+ {},
142
+ )
143
+ const err = await new Promise((resolve) => {
144
+ h.once('ready', () => resolve(null))
145
+ h.once('error', resolve)
146
+ })
147
+ assert.ok(err, 'expected an error for truncated TODO')
148
+ })
149
+
150
+ it('emits error on too-long declared TODO length', async () => {
151
+ // Recreate fixture in case a prior run renamed it to the error queue
152
+ makeToolongFixture()
153
+ const h = new Hmail('1509000000000_1509000000000_0_99999_ToLong_1_haraka', TOOLONG_FIXTURE, {})
154
+ const err = await new Promise((resolve) => {
155
+ h.once('ready', () => resolve(null))
156
+ h.once('error', resolve)
157
+ })
158
+ assert.ok(err, 'expected an error for oversized TODO')
159
+ })
160
+
161
+ it('skips zero-length file without crash', async () => {
162
+ const h = new Hmail('1507509981169_1507509981169_0_61403_e0Y0Ym_2_zero', 'test/queue/zero-length', {})
163
+ await new Promise((resolve) => {
164
+ h.once('ready', resolve)
165
+ h.once('error', resolve)
166
+ })
167
+ assert.ok(h)
168
+ })
169
+
170
+ it('releases queue slot when stat fails on exhausted-retry item (regression #3560)', async () => {
171
+ // When fs.stat fails and num_failures already equals temp_fail_intervals.length,
172
+ // temp_fail() calls convert_temp_failed_to_bounce() while this.todo is null.
173
+ // That must not crash, and must call next_cb() to release the queue slot.
174
+ // attempts=12 in the filename causes num_failures=12 at construction; after
175
+ // temp_fail() increments it to 13 (> temp_fail_intervals.length=12) the
176
+ // overflow path fires with this.todo still null.
177
+ const fname = '1508455115683_1508455115683_12_90253_9Q4o4V_1_haraka'
178
+ const h = new Hmail(fname, '/nonexistent/path/that/cannot/be/stat/ed', {})
179
+ await new Promise((resolve, reject) => {
180
+ const timer = setTimeout(() => reject(new Error('next_cb was never called')), 2000)
181
+ h.next_cb = () => {
182
+ clearTimeout(timer)
183
+ resolve()
184
+ }
185
+ })
186
+ assert.equal(h.todo, null, 'todo must remain null — file was never readable')
187
+ })
188
+
189
+ it('lifecycle: reads and writes a queue file', async () => {
190
+ const h = new Hmail('1507509981169_1507509981169_0_61403_e0Y0Ym_2_qfile', 'test/fixtures/todo_qfile.txt', {})
191
+
192
+ await onEvent(h, 'ready')
193
+
194
+ const tmpfile = path.resolve('test', 'test-queue', 'delete-me')
195
+ await fs.promises.mkdir(path.dirname(tmpfile), { recursive: true })
196
+ const ws = new fs.WriteStream(tmpfile)
197
+
198
+ await new Promise((resolve, reject) => {
199
+ outbound.build_todo(h.todo, ws, () => {
200
+ const ds = h.data_stream()
201
+ ds.pipe(ws)
202
+ ws.on('close', resolve)
203
+ ws.on('error', reject)
204
+ })
205
+ })
206
+
207
+ assert.equal(fs.statSync(tmpfile).size, 4204)
208
+ fs.unlinkSync(tmpfile)
209
+ })
210
+ })
@@ -0,0 +1,385 @@
1
+ 'use strict'
2
+
3
+ const { describe, it, beforeEach, afterEach } = require('node:test')
4
+ const assert = require('node:assert')
5
+ const fs = require('node:fs')
6
+ const path = require('node:path')
7
+
8
+ const constants = require('haraka-constants')
9
+
10
+ const lines = [
11
+ 'From: John Johnson <john@example.com>',
12
+ 'To: Jane Johnson <jane@example.com>',
13
+ "Subject: What's for dinner?",
14
+ '',
15
+ "I'm hungry.",
16
+ '',
17
+ ]
18
+
19
+ describe('outbound', () => {
20
+ it('converts \\n and \\r\\n line endings to \\r\\n', () => {
21
+ for (const ending of ['\n', '\r\n']) {
22
+ let contents = lines.join(ending)
23
+ let result = ''
24
+
25
+ let match
26
+ const re = /^([^\n]*\n?)/
27
+ while ((match = re.exec(contents))) {
28
+ let line = match[1]
29
+ line = line.replace(/\r?\n?$/, '\r\n')
30
+ result += line
31
+ contents = contents.substring(match[1].length)
32
+ if (contents.length === 0) break
33
+ }
34
+
35
+ assert.deepEqual(lines.join('\r\n'), result)
36
+ }
37
+ })
38
+
39
+ it('log_methods added to HMailItem prototype', () => {
40
+ const levels = ['DATA', 'PROTOCOL', 'DEBUG', 'INFO', 'NOTICE', 'WARN', 'ERROR', 'CRIT', 'ALERT', 'EMERG']
41
+ // Load via outbound/index to avoid circular-dep boot-order issue
42
+ const HMailItem = require('../../outbound').HMailItem
43
+ for (const level of levels) {
44
+ assert.ok(HMailItem.prototype[`log${level.toLowerCase()}`], `log method for ${level}`)
45
+ }
46
+ })
47
+
48
+ it('set_temp_fail_intervals coverage', () => {
49
+ const config = require('../../outbound/config')
50
+ assert.deepEqual(
51
+ config.cfg.temp_fail_intervals,
52
+ [64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072],
53
+ )
54
+
55
+ config.cfg.temp_fail_intervals = '10s, 1m*2'
56
+ config.set_temp_fail_intervals()
57
+ assert.deepEqual(config.cfg.temp_fail_intervals, [10, 60, 60])
58
+
59
+ config.cfg.temp_fail_intervals = '30s, 1m, 5m, 9m, 15m*3, 30m*2, 1h*3, 2h*3, 1d'
60
+ config.set_temp_fail_intervals()
61
+ assert.deepEqual(
62
+ config.cfg.temp_fail_intervals,
63
+ [30, 60, 300, 540, 900, 900, 900, 1800, 1800, 3600, 3600, 3600, 7200, 7200, 7200, 86400],
64
+ )
65
+
66
+ config.cfg.temp_fail_intervals = 'none'
67
+ config.set_temp_fail_intervals()
68
+ assert.deepEqual(config.cfg.temp_fail_intervals, [])
69
+
70
+ config.cfg.temp_fail_intervals = '60 min'
71
+ config.set_temp_fail_intervals()
72
+ assert.deepEqual(
73
+ config.cfg.temp_fail_intervals,
74
+ [64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072],
75
+ )
76
+ })
77
+
78
+ describe('get_tls_options', () => {
79
+ let outbound, obtls
80
+
81
+ beforeEach(async () => {
82
+ process.env.HARAKA_TEST_DIR = path.resolve('test')
83
+ outbound = require('../../outbound')
84
+ obtls = require('../../outbound/tls')
85
+ const tls_socket = require('../../tls_socket')
86
+
87
+ const testDir = path.resolve('test')
88
+ outbound.config = outbound.config.module_config(testDir)
89
+ obtls.test_config(tls_socket.config.module_config(testDir), outbound.config)
90
+ await new Promise((resolve) => obtls.init(resolve))
91
+ })
92
+
93
+ afterEach(() => {
94
+ delete process.env.HARAKA_TEST_DIR
95
+ })
96
+
97
+ it('gets TLS properties from tls.ini.outbound', () => {
98
+ const tls_config = obtls.get_tls_options({ exchange: 'mail.example.com' })
99
+ assert.deepEqual(tls_config, {
100
+ servername: 'mail.example.com',
101
+ key: fs.readFileSync(path.resolve('test', 'config', 'outbound_tls_key.pem')),
102
+ cert: fs.readFileSync(path.resolve('test', 'config', 'outbound_tls_cert.pem')),
103
+ dhparam: fs.readFileSync(path.resolve('test', 'config', 'dhparams.pem')),
104
+ ciphers: 'ECDHE-RSA-AES256-GCM-SHA384',
105
+ minVersion: 'TLSv1',
106
+ rejectUnauthorized: false,
107
+ requestCert: false,
108
+ honorCipherOrder: false,
109
+ redis: { disable_for_failed_hosts: false },
110
+ no_tls_hosts: ['127.0.0.2', '192.168.31.1/24'],
111
+ force_tls_hosts: ['first.example.com', 'second.example.net'],
112
+ })
113
+ })
114
+ })
115
+
116
+ describe('build_todo', () => {
117
+ let outbound
118
+
119
+ beforeEach(() => {
120
+ outbound = require('../../outbound')
121
+ try {
122
+ fs.unlinkSync('test/queue/multibyte')
123
+ fs.unlinkSync('test/queue/plain')
124
+ } catch (ignore) {}
125
+ })
126
+
127
+ it('saves a plain queue file', () => {
128
+ const todo = JSON.parse(
129
+ '{"queue_time":1507509981169,"domain":"redacteed.com","rcpt_to":[{"original":"<postmaster@redacteed.com>","original_host":"redacteed.com","host":"redacteed.com","user":"postmaster"}],"mail_from":{"original":"<matt@tnpi.net>","original_host":"tnpi.net","host":"tnpi.net","user":"matt"},"notes":{"authentication_results":["spf=pass smtp.mailfrom=tnpi.net"],"spf_mail_result":"Pass","spf_mail_record":"v=spf1 a mx include:mx.theartfarm.com ?include:forwards._spf.tnpi.net include:lists._spf.tnpi.net -all","attachment_count":0,"attachments":[{"ctype":"application/pdf","filename":"FileWithoutAccent Chars.pdf","extension":".pdf","md5":"6c1d5f5c047cff3f6320b1210970bdf6"}],"attachment_ctypes":["application/pdf","multipart/mixed","text/plain","application/pdf"],"attachment_files":["FileWithoutaccent Chars.pdf"],"attachment_archive_files":[]},"uuid":"1D5483B0-3E00-4280-A961-3AFD2017B4FC.1"}',
130
+ )
131
+ const fd = fs.openSync('test/queue/plain', 'w')
132
+ const ws = new fs.createWriteStream('test/queue/plain', { fd, flags: constants.WRITE_EXCL })
133
+ ws.on('error', (e) => console.error(e))
134
+ outbound.build_todo(todo, ws, () => {
135
+ ws.write(Buffer.from('This is the message body'))
136
+ fs.fsync(fd, () => ws.close())
137
+ })
138
+ assert.ok(true)
139
+ })
140
+
141
+ it('saves a queue file with multibyte chars', () => {
142
+ const todo = JSON.parse(
143
+ '{"queue_time":1507509981169,"domain":"redacteed.com","rcpt_to":[{"original":"<postmaster@redacteed.com>","original_host":"redacteed.com","host":"redacteed.com","user":"postmaster"}],"mail_from":{"original":"<matt@tnpi.net>","original_host":"tnpi.net","host":"tnpi.net","user":"matt"},"notes":{"authentication_results":["spf=pass smtp.mailfrom=tnpi.net"],"spf_mail_result":"Pass","spf_mail_record":"v=spf1 a mx include:mx.theartfarm.com ?include:forwards._spf.tnpi.net include:lists._spf.tnpi.net -all","attachment_count":0,"attachments":[{"ctype":"application/pdf","filename":"FileW\\u00eeth\\u00c1ccent Chars.pdf","extension":".pdf","md5":"6c1d5f5c047cff3f6320b1210970bdf6"}],"attachment_ctypes":["application/pdf","multipart/mixed","text/plain","application/pdf"],"attachment_files":["FileW\\u00eeth\\u00c1ccent Chars.pdf"],"attachment_archive_files":[]},"uuid":"1D5483B0-3E00-4280-A961-3AFD2017B4FC.1"}',
144
+ )
145
+ const fd = fs.openSync('test/queue/multibyte', 'w')
146
+ const ws = new fs.WriteStream('test/queue/multibyte', { fd, flags: constants.WRITE_EXCL })
147
+ ws.on('error', (e) => console.error(e))
148
+ outbound.build_todo(todo, ws, () => {
149
+ ws.write(Buffer.from('This is the message body'))
150
+ fs.fsync(fd, () => ws.close())
151
+ })
152
+ assert.ok(true)
153
+ })
154
+
155
+ it('waits for drain when stream backpressure is applied', async () => {
156
+ const todo = {
157
+ queue_time: Date.now(),
158
+ domain: 'example.com',
159
+ rcpt_to: [],
160
+ mail_from: {},
161
+ notes: {},
162
+ uuid: 'u1',
163
+ }
164
+ let drained = false
165
+
166
+ await new Promise((resolve) => {
167
+ const ws = {
168
+ write() {
169
+ return false
170
+ },
171
+ once(event, cb) {
172
+ assert.equal(event, 'drain')
173
+ setImmediate(() => {
174
+ drained = true
175
+ cb()
176
+ resolve()
177
+ })
178
+ },
179
+ }
180
+ outbound.build_todo(todo, ws, () => {})
181
+ })
182
+
183
+ assert.equal(drained, true)
184
+ })
185
+ })
186
+
187
+ describe('send_trans_email', () => {
188
+ const queueDir = path.resolve('test', 'test-queue')
189
+
190
+ beforeEach(() => {
191
+ process.env.HARAKA_TEST_DIR = path.resolve('test')
192
+ fs.mkdirSync(queueDir, { recursive: true })
193
+ })
194
+
195
+ afterEach(() => {
196
+ delete process.env.HARAKA_TEST_DIR
197
+ try {
198
+ for (const f of fs.readdirSync(queueDir)) {
199
+ fs.unlinkSync(path.join(queueDir, f))
200
+ }
201
+ } catch (ignore) {}
202
+ })
203
+
204
+ // Regression test for haraka/Haraka#3551:
205
+ // When dkim_verify (data_post) pipes the message_stream and DKIMVerifyStream
206
+ // fires its callback early via process.nextTick (no DKIM-Signature found),
207
+ // the chain runs synchronously into process_delivery → pipe() while the
208
+ // first pipe is still in flight. pre_send_trans_email_respond must yield
209
+ // (via setImmediate) before opening a new pipe.
210
+ it('yields to setImmediate before opening process_delivery pipes', async () => {
211
+ const stream = require('node:stream')
212
+ const Transaction = require('../../transaction')
213
+ const Address = require('../../address').Address
214
+ const outbound = require('../../outbound')
215
+ const plugins = require('../../plugins')
216
+
217
+ const txn = Transaction.createTransaction()
218
+ const origRunHooks = plugins.run_hooks
219
+ try {
220
+ txn.mail_from = new Address('<from@example.com>')
221
+ txn.rcpt_to = [new Address('<to@example.com>')]
222
+ txn.message_stream.add_line(Buffer.from('From: from@example.com\r\n'))
223
+ txn.message_stream.add_line(Buffer.from('To: to@example.com\r\n'))
224
+ txn.message_stream.add_line(Buffer.from('\r\n'))
225
+ txn.message_stream.add_line(Buffer.from('body\r\n'))
226
+ await new Promise((r) => txn.message_stream.add_line_end(r))
227
+
228
+ // Start a pipe on the message_stream and fire a synchronous callback
229
+ // before it drains — this models what dkim_verify does.
230
+ const verifierFiredCb = new Promise((resolve) => {
231
+ let scheduled = false
232
+ const verifier = new stream.Writable({
233
+ write(_chunk, _enc, cb) {
234
+ if (!scheduled) {
235
+ scheduled = true
236
+ process.nextTick(resolve)
237
+ }
238
+ cb()
239
+ },
240
+ })
241
+ txn.message_stream.pipe(verifier)
242
+ })
243
+ await verifierFiredCb
244
+
245
+ // Now invoke send_trans_email — its pre_send_trans_email_respond
246
+ // should yield (await setImmediate) before calling process_delivery,
247
+ // letting the verifier pipe drain so the new pipe can succeed.
248
+ await new Promise((resolve, reject) => {
249
+ // Stub the heavy bits: we only care that the chain doesn't throw
250
+ // "Cannot pipe while currently piping" before queuing happens.
251
+ plugins.run_hooks = (hook, obj) => {
252
+ if (hook === 'pre_send_trans_email') {
253
+ // Mimic empty-hook synchronous callback (no plugins)
254
+ obj.pre_send_trans_email_respond(constants.cont).catch(reject)
255
+ } else {
256
+ origRunHooks.call(plugins, hook, obj)
257
+ }
258
+ }
259
+
260
+ outbound.send_trans_email(txn, (retval) => {
261
+ if (retval === constants.ok) resolve()
262
+ else reject(new Error(`unexpected retval ${retval}`))
263
+ })
264
+ })
265
+ } finally {
266
+ plugins.run_hooks = origRunHooks
267
+ txn.message_stream.destroy()
268
+ }
269
+ })
270
+
271
+ it('adds missing Message-Id/Date and prepends Received before queueing', async () => {
272
+ process.env.HARAKA_TEST_DIR = path.resolve('test')
273
+ const Address = require('../../address').Address
274
+ const outbound = require('../../outbound')
275
+ const plugins = require('../../plugins')
276
+
277
+ const added = []
278
+ const leading = []
279
+ const queued = []
280
+ const transaction = {
281
+ uuid: 'txn-add-headers',
282
+ header: {
283
+ get_all() {
284
+ return []
285
+ },
286
+ get() {
287
+ return null
288
+ },
289
+ },
290
+ rcpt_to: [new Address('<user@example.com>')],
291
+ notes: {},
292
+ add_header(name, value) {
293
+ added.push([name, value])
294
+ },
295
+ remove_header() {},
296
+ add_leading_header(name, value) {
297
+ leading.push([name, value])
298
+ },
299
+ results: {
300
+ add() {},
301
+ },
302
+ }
303
+
304
+ const originalRunHooks = plugins.run_hooks
305
+ const originalProcessDelivery = outbound.process_delivery
306
+ const originalPush = outbound.delivery_queue.push
307
+ outbound.delivery_queue.push = (hmail) => {
308
+ queued.push(hmail)
309
+ }
310
+ outbound.process_delivery = async (_okPaths, _todo, hmails) => {
311
+ hmails.push({ queued: true })
312
+ }
313
+ plugins.run_hooks = (hook, conn) => {
314
+ if (hook === 'pre_send_trans_email') {
315
+ conn.pre_send_trans_email_respond(constants.cont)
316
+ }
317
+ }
318
+
319
+ try {
320
+ const result = await new Promise((resolve) => {
321
+ outbound.send_trans_email(transaction, (retval, msg) => resolve({ retval, msg }))
322
+ })
323
+
324
+ assert.equal(result.retval, constants.ok)
325
+ assert.match(result.msg, /Message Queued/)
326
+ assert.equal(queued.length, 1)
327
+ assert.equal(
328
+ added.some(([name]) => name === 'Message-Id'),
329
+ true,
330
+ )
331
+ assert.equal(
332
+ added.some(([name]) => name === 'Date'),
333
+ true,
334
+ )
335
+ assert.equal(leading[0][0], 'Received')
336
+ } finally {
337
+ plugins.run_hooks = originalRunHooks
338
+ outbound.process_delivery = originalProcessDelivery
339
+ outbound.delivery_queue.push = originalPush
340
+ delete process.env.HARAKA_TEST_DIR
341
+ }
342
+ })
343
+ })
344
+
345
+ describe('timer_queue', () => {
346
+ let ob_timer_queue
347
+
348
+ beforeEach(() => {
349
+ process.env.HARAKA_TEST_DIR = path.resolve('test')
350
+ require('../../outbound')
351
+ const { TimerQueue } = require('haraka-utils')
352
+ ob_timer_queue = new TimerQueue(500)
353
+ })
354
+
355
+ afterEach(() => {
356
+ delete process.env.HARAKA_TEST_DIR
357
+ ob_timer_queue.shutdown()
358
+ })
359
+
360
+ it('has initial length of 0', () => {
361
+ assert.equal(ob_timer_queue.length(), 0)
362
+ })
363
+
364
+ it('can add items', () => {
365
+ ob_timer_queue.add('1', 1000)
366
+ ob_timer_queue.add('2', 2000)
367
+ assert.equal(ob_timer_queue.length(), 2)
368
+ })
369
+
370
+ it('can drain items', () => {
371
+ ob_timer_queue.add('1', 1000)
372
+ ob_timer_queue.add('2', 2000)
373
+ ob_timer_queue.drain()
374
+ assert.equal(ob_timer_queue.length(), 0)
375
+ })
376
+
377
+ it('can discard items by id', () => {
378
+ ob_timer_queue.add('1', 1000)
379
+ ob_timer_queue.add('2', 2000)
380
+ ob_timer_queue.discard('2')
381
+ assert.equal(ob_timer_queue.length(), 1)
382
+ assert.equal(ob_timer_queue.queue[0].id, '1')
383
+ })
384
+ })
385
+ })