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,207 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert/strict')
4
+ const { describe, it, beforeEach } = require('node:test')
5
+
6
+ const fixtures = require('haraka-test-fixtures')
7
+ const { makeConnection, makePlugin } = fixtures
8
+ const outbound = require('../../outbound')
9
+ const { TimerQueue } = require('haraka-utils')
10
+
11
+ const _set_up = () => {
12
+ this.plugin = makePlugin('status', { register: false })
13
+ this.plugin.outbound = outbound
14
+
15
+ this.connection = makeConnection()
16
+ this.connection.remote.is_local = true
17
+ }
18
+
19
+ describe('status', () => {
20
+ describe('register', () => {
21
+ beforeEach(_set_up)
22
+
23
+ it('loads the status plugin', () => {
24
+ assert.equal('status', this.plugin.name)
25
+ })
26
+ })
27
+
28
+ describe('access', () => {
29
+ beforeEach(_set_up)
30
+
31
+ it('remote', (t, done) => {
32
+ this.connection.remote.is_local = false
33
+ this.plugin.hook_unrecognized_command(
34
+ (code) => {
35
+ assert.equal(DENY, code)
36
+ done()
37
+ },
38
+ this.connection,
39
+ ['STATUS', 'POOL LIST'],
40
+ )
41
+ })
42
+ })
43
+
44
+ describe('pools', () => {
45
+ beforeEach(_set_up)
46
+
47
+ it('list_pools', (t, done) => {
48
+ this.connection.respond = (code, message) => {
49
+ const data = JSON.parse(message)
50
+ assert.equal('object', typeof data) // there should be one pools array for noncluster and more for cluster
51
+ done()
52
+ }
53
+ this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'POOL LIST'])
54
+ })
55
+ })
56
+
57
+ describe('queues', () => {
58
+ beforeEach(_set_up)
59
+
60
+ it('inspect_queue', (t, done) => {
61
+ // should list delivery_queue and temp_fail_queue per cluster children
62
+ outbound.temp_fail_queue = new TimerQueue(10)
63
+ outbound.temp_fail_queue.add('file1', 100, () => {})
64
+ outbound.temp_fail_queue.add('file2', 100, () => {})
65
+
66
+ this.connection.respond = (code, message) => {
67
+ const data = JSON.parse(message)
68
+ assert.equal(0, data.delivery_queue.length)
69
+ assert.equal(2, data.temp_fail_queue.length)
70
+ done()
71
+ }
72
+ this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'QUEUE INSPECT'])
73
+ })
74
+
75
+ it('stat_queue', (t, done) => {
76
+ // should list files only
77
+ this.connection.respond = (code, message) => {
78
+ const data = JSON.parse(message)
79
+ assert.ok(/^\d+\/\d+\/\d+$/.test(data))
80
+ done()
81
+ }
82
+ this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'QUEUE STATS'])
83
+ })
84
+
85
+ it('list_queue', (t, done) => {
86
+ // should list files only
87
+ this.connection.respond = (code, message) => {
88
+ const data = JSON.parse(message)
89
+ assert.equal(0, data.length)
90
+ done()
91
+ }
92
+ this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'QUEUE LIST'])
93
+ })
94
+
95
+ it('discard_from_queue', (t, done) => {
96
+ const self = this
97
+
98
+ outbound.temp_fail_queue = new TimerQueue(10)
99
+ outbound.temp_fail_queue.add('file1', 10, () => {
100
+ assert.ok(false, 'This callback should not be called')
101
+ done()
102
+ })
103
+
104
+ outbound.temp_fail_queue.add('file2', 2000, () => {})
105
+
106
+ this.plugin.hook_unrecognized_command(
107
+ () => {
108
+ self.connection.respond = (code, message) => {
109
+ const data = JSON.parse(message)
110
+ assert.equal(1, data.temp_fail_queue.length)
111
+ done()
112
+ }
113
+ self.plugin.hook_unrecognized_command(() => {}, self.connection, ['STATUS', 'QUEUE INSPECT'])
114
+ },
115
+ this.connection,
116
+ ['STATUS', 'QUEUE DISCARD file1'],
117
+ )
118
+ })
119
+
120
+ it('push_email_at_queue', (t, done) => {
121
+ const timeout = setTimeout(() => {
122
+ assert.ok(false, 'Timeout')
123
+ done()
124
+ }, 1000)
125
+
126
+ outbound.temp_fail_queue.add('file', 1500, () => {
127
+ clearTimeout(timeout)
128
+
129
+ assert.ok(true)
130
+ done()
131
+ })
132
+
133
+ this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'QUEUE PUSH file'])
134
+ })
135
+ })
136
+
137
+ describe('merge_worker_responses', () => {
138
+ beforeEach(_set_up)
139
+
140
+ it('POOL LIST merges objects from all workers', () => {
141
+ const result = JSON.parse(
142
+ JSON.stringify(
143
+ this.plugin.merge_worker_responses('POOL LIST', [
144
+ { 'host1:25': { inUse: 1, size: 3 } },
145
+ { 'host2:25': { inUse: 0, size: 2 } },
146
+ {},
147
+ ]),
148
+ ),
149
+ )
150
+ assert.deepEqual(result, {
151
+ 'host1:25': { inUse: 1, size: 3 },
152
+ 'host2:25': { inUse: 0, size: 2 },
153
+ })
154
+ })
155
+
156
+ it('POOL LIST with all empty workers returns empty object', () => {
157
+ const result = JSON.parse(JSON.stringify(this.plugin.merge_worker_responses('POOL LIST', [{}, {}, {}])))
158
+ assert.deepEqual(result, {})
159
+ })
160
+
161
+ it('QUEUE INSPECT merges queues from all workers', () => {
162
+ const result = JSON.parse(
163
+ JSON.stringify(
164
+ this.plugin.merge_worker_responses('QUEUE INSPECT', [
165
+ { delivery_queue: [{ id: 'a' }], temp_fail_queue: [{ id: 'x', fire_time: 1 }] },
166
+ { delivery_queue: [{ id: 'b' }], temp_fail_queue: [] },
167
+ { delivery_queue: [], temp_fail_queue: [{ id: 'y', fire_time: 2 }] },
168
+ ]),
169
+ ),
170
+ )
171
+ assert.deepEqual(result, {
172
+ delivery_queue: [{ id: 'a' }, { id: 'b' }],
173
+ temp_fail_queue: [
174
+ { id: 'x', fire_time: 1 },
175
+ { id: 'y', fire_time: 2 },
176
+ ],
177
+ })
178
+ })
179
+
180
+ it('QUEUE INSPECT with all empty queues returns empty lists', () => {
181
+ const result = JSON.parse(
182
+ JSON.stringify(
183
+ this.plugin.merge_worker_responses('QUEUE INSPECT', [
184
+ { delivery_queue: [], temp_fail_queue: [] },
185
+ { delivery_queue: [], temp_fail_queue: [] },
186
+ ]),
187
+ ),
188
+ )
189
+ assert.deepEqual(result, { delivery_queue: [], temp_fail_queue: [] })
190
+ })
191
+
192
+ it('QUEUE STATS sums across workers', () => {
193
+ const result = this.plugin.merge_worker_responses('QUEUE STATS', ['1/2/3', '0/1/0', '2/0/1'])
194
+ assert.equal(result, '3/3/4')
195
+ })
196
+
197
+ it('QUEUE STATS with all zeros', () => {
198
+ const result = this.plugin.merge_worker_responses('QUEUE STATS', ['0/0/0', '0/0/0', '0/0/0'])
199
+ assert.equal(result, '0/0/0')
200
+ })
201
+
202
+ it('unknown command returns results array unchanged', () => {
203
+ const result = this.plugin.merge_worker_responses('POOL UNKNOWN', [{ foo: 1 }, { foo: 2 }])
204
+ assert.equal(result.length, 2)
205
+ })
206
+ })
207
+ })
@@ -0,0 +1,90 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert/strict')
4
+ const { describe, it, beforeEach } = require('node:test')
5
+
6
+ const { makeConnection, makePlugin } = require('haraka-test-fixtures')
7
+
8
+ describe('tarpit', () => {
9
+ let plugin
10
+
11
+ beforeEach(() => {
12
+ plugin = makePlugin('tarpit', { register: false })
13
+ plugin.config.get = () => ({ main: {} })
14
+ })
15
+
16
+ describe('register', () => {
17
+ it('registers tarpit on all default hooks', () => {
18
+ const registered = []
19
+ plugin.register_hook = (hook) => registered.push(hook)
20
+ plugin.register()
21
+ assert.ok(registered.includes('connect'))
22
+ assert.ok(registered.includes('ehlo'))
23
+ assert.ok(registered.includes('mail'))
24
+ assert.ok(registered.includes('rcpt'))
25
+ assert.ok(registered.includes('data'))
26
+ assert.ok(registered.includes('queue'))
27
+ assert.ok(registered.includes('quit'))
28
+ })
29
+
30
+ it('registers only configured hooks when hooks_to_delay is set', () => {
31
+ plugin.config.get = () => ({ main: { hooks_to_delay: 'ehlo, mail' } })
32
+ const registered = []
33
+ plugin.register_hook = (hook) => registered.push(hook)
34
+ plugin.register()
35
+ assert.deepEqual(registered, ['ehlo', 'mail'])
36
+ })
37
+ })
38
+
39
+ describe('tarpit', () => {
40
+ let conn
41
+
42
+ beforeEach(() => {
43
+ conn = makeConnection({ withTxn: true })
44
+ })
45
+
46
+ it('calls next immediately when no transaction', (t, done) => {
47
+ conn.transaction = null
48
+ plugin.tarpit((rc) => {
49
+ assert.equal(rc, undefined)
50
+ done()
51
+ }, conn)
52
+ })
53
+
54
+ it('calls next immediately when no tarpit delay set', (t, done) => {
55
+ // No tarpit note on connection or transaction
56
+ plugin.tarpit((rc) => {
57
+ assert.equal(rc, undefined)
58
+ done()
59
+ }, conn)
60
+ })
61
+
62
+ it('calls next immediately when connection.notes.tarpit is 0', (t, done) => {
63
+ conn.notes.tarpit = 0
64
+ plugin.tarpit((rc) => {
65
+ assert.equal(rc, undefined)
66
+ done()
67
+ }, conn)
68
+ })
69
+
70
+ it('delays and calls next when connection.notes.tarpit is set', { timeout: 3000 }, (t, done) => {
71
+ conn.notes.tarpit = 0.1
72
+ const start = Date.now()
73
+ plugin.tarpit((rc) => {
74
+ assert.equal(rc, undefined)
75
+ assert.ok(Date.now() - start >= 90, 'should have waited ~100ms')
76
+ done()
77
+ }, conn)
78
+ })
79
+
80
+ it('uses transaction.notes.tarpit when connection note is absent', { timeout: 3000 }, (t, done) => {
81
+ conn.transaction.notes.tarpit = 0.1
82
+ const start = Date.now()
83
+ plugin.tarpit((rc) => {
84
+ assert.equal(rc, undefined)
85
+ assert.ok(Date.now() - start >= 90)
86
+ done()
87
+ }, conn)
88
+ })
89
+ })
90
+ })
@@ -0,0 +1,86 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert/strict')
4
+ const path = require('node:path')
5
+ const { describe, it, beforeEach } = require('node:test')
6
+
7
+ const { makeConnection, makePlugin } = require('haraka-test-fixtures')
8
+
9
+ const _set_up = () => {
10
+ this.plugin = makePlugin('tls', { register: false, configDir: 'test' })
11
+ this.connection = makeConnection()
12
+
13
+ // use test/config instead of ./config
14
+ this.plugin.net_utils.config = this.plugin.net_utils.config.module_config(path.resolve('test'))
15
+
16
+ this.plugin.tls_opts = {}
17
+ }
18
+
19
+ describe('tls', () => {
20
+ beforeEach(_set_up)
21
+
22
+ const methods = ['register', 'upgrade_connection', 'advertise_starttls', 'emit_upgrade_msg']
23
+ for (const method of methods) {
24
+ it(`has function ${method}`, () => {
25
+ assert.equal(typeof this.plugin[method], 'function')
26
+ })
27
+ }
28
+
29
+ describe('register', () => {
30
+ it('with certs, should register hooks', () => {
31
+ this.plugin.register()
32
+ assert.ok(Object.keys(this.plugin.hooks).length)
33
+ })
34
+ })
35
+
36
+ describe('emit_upgrade_msg', () => {
37
+ it('should emit a log message', () => {
38
+ assert.equal(
39
+ this.plugin.emit_upgrade_msg(this.connection, true, '', {
40
+ subject: {
41
+ CN: 'TLS.subject',
42
+ O: 'TLS.org',
43
+ },
44
+ }),
45
+ 'secured: verified=true cn="TLS.subject" organization="TLS.org"',
46
+ )
47
+ })
48
+
49
+ it('should emit a log message with error', () => {
50
+ assert.equal(
51
+ this.plugin.emit_upgrade_msg(this.connection, true, 'oops', {
52
+ subject: {
53
+ CN: 'TLS.subject',
54
+ O: 'TLS.org',
55
+ },
56
+ }),
57
+ 'secured: verified=true error="oops" cn="TLS.subject" organization="TLS.org"',
58
+ )
59
+ })
60
+ })
61
+
62
+ describe('upgrade_connection (STARTTLS injection)', () => {
63
+ // RFC 3207 §4: data pipelined after STARTTLS but before the TLS
64
+ // handshake must be discarded, not processed on the cleartext channel.
65
+ it('discards pipelined plaintext before the TLS handshake', () => {
66
+ const c = this.connection
67
+ c.tls = { advertised: true }
68
+ c.notes = {}
69
+ // attacker pipelined an injected command after STARTTLS
70
+ c.current_data = Buffer.from('RCPT TO:<victim@example.com>\r\n')
71
+ let dataAtUpgrade = 'UPGRADE_NOT_CALLED'
72
+ c.client = {
73
+ upgrade() {
74
+ dataAtUpgrade = c.current_data
75
+ },
76
+ }
77
+ c.respond = () => {} // bypass the real _process_data path
78
+ this.plugin.timeout = 0
79
+
80
+ this.plugin.upgrade_connection(() => {}, c, ['STARTTLS'])
81
+
82
+ assert.equal(dataAtUpgrade, null, 'buffer cleared before upgrade()')
83
+ assert.equal(c.current_data, null)
84
+ })
85
+ })
86
+ })
@@ -0,0 +1,21 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert/strict')
4
+ const { describe, it } = require('node:test')
5
+
6
+ const { makePlugin } = require('haraka-test-fixtures')
7
+
8
+ describe('toobusy', () => {
9
+ describe('register', () => {
10
+ it('handles missing toobusy-js gracefully (does not throw)', () => {
11
+ const plugin = makePlugin('toobusy', { register: false })
12
+ // toobusy-js is not installed; register should catch the error and return
13
+ let registered = false
14
+ plugin.register_hook = () => {
15
+ registered = true
16
+ }
17
+ assert.doesNotThrow(() => plugin.register())
18
+ assert.equal(registered, false, 'hook should not be registered without toobusy-js')
19
+ })
20
+ })
21
+ })
@@ -0,0 +1,119 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert/strict')
4
+ const { describe, it, beforeEach } = require('node:test')
5
+
6
+ const { makeConnection, makePlugin } = require('haraka-test-fixtures')
7
+
8
+ const _set_up = () => {
9
+ this.plugin = makePlugin('xclient', { register: false })
10
+ this.connection = makeConnection()
11
+ this.connection.capabilities = []
12
+ }
13
+
14
+ describe('xclient', () => {
15
+ beforeEach(_set_up)
16
+
17
+ describe('hook_capabilities', () => {
18
+ const cases = [
19
+ { desc: 'adds XCLIENT for loopback IPv4 (127.0.0.1)', ip: '127.0.0.1', expected: true },
20
+ { desc: 'adds XCLIENT for loopback IPv6 (::1)', ip: '::1', expected: true },
21
+ { desc: 'does not add XCLIENT for non-loopback IP', ip: '10.0.0.1', expected: false },
22
+ ]
23
+
24
+ for (const { desc, ip, expected } of cases) {
25
+ it(desc, async () => {
26
+ this.connection.remote.ip = ip
27
+ await new Promise((resolve) => this.plugin.hook_capabilities(resolve, this.connection))
28
+ const hasXclient = this.connection.capabilities.some((c) => c.startsWith('XCLIENT'))
29
+ assert.equal(hasXclient, expected)
30
+ })
31
+ }
32
+ })
33
+
34
+ describe('hook_unrecognized_command', () => {
35
+ const callHook = (params) =>
36
+ new Promise((resolve) => {
37
+ this.plugin.hook_unrecognized_command((code) => resolve(code), this.connection, params)
38
+ })
39
+
40
+ const cases = [
41
+ {
42
+ desc: 'ignores non-XCLIENT commands',
43
+ params: ['EHLO', 'example.com'],
44
+ check: (code) => assert.equal(code, undefined),
45
+ },
46
+ {
47
+ desc: 'denies XCLIENT when transaction is in progress',
48
+ setup: () => {
49
+ this.connection = makeConnection({ withTxn: true })
50
+ this.connection.capabilities = []
51
+ },
52
+ params: ['XCLIENT', 'ADDR=127.0.0.1'],
53
+ check: (code) => assert.equal(code, DENY),
54
+ },
55
+ {
56
+ desc: 'denies XCLIENT from disallowed IP',
57
+ setup: () => {
58
+ this.connection.remote.ip = '10.0.0.1'
59
+ },
60
+ params: ['XCLIENT', 'ADDR=127.0.0.2'],
61
+ check: (code) => assert.equal(code, DENY),
62
+ },
63
+ {
64
+ desc: 'denies XCLIENT with no valid IP address',
65
+ setup: () => {
66
+ this.connection.remote.ip = '127.0.0.1'
67
+ },
68
+ params: ['XCLIENT', 'NAME=example.com'],
69
+ check: (code) => assert.equal(code, DENY),
70
+ },
71
+ {
72
+ desc: 'accepts XCLIENT with valid IPv4 ADDR from allowed host',
73
+ setup: () => {
74
+ this.connection.remote.ip = '127.0.0.1'
75
+ },
76
+ params: ['XCLIENT', 'ADDR=1.2.3.4'],
77
+ check: (code) => assert.ok(code === NEXT_HOOK || code === undefined),
78
+ },
79
+ {
80
+ desc: 'accepts XCLIENT with valid IPv6 ADDR from allowed host',
81
+ setup: () => {
82
+ this.connection.remote.ip = '127.0.0.1'
83
+ },
84
+ params: ['XCLIENT', 'ADDR=IPV6:2001:db8::1'],
85
+ check: (code) => assert.ok(code === NEXT_HOOK || code === undefined),
86
+ },
87
+ {
88
+ desc: 'accepts XCLIENT with ADDR and NAME, skipping rdns lookup',
89
+ setup: () => {
90
+ this.connection.remote.ip = '127.0.0.1'
91
+ },
92
+ params: ['XCLIENT', 'ADDR=1.2.3.4 NAME=example.com'],
93
+ check: (code) => assert.equal(code, NEXT_HOOK),
94
+ },
95
+ ]
96
+
97
+ for (const { desc, setup, params, check } of cases) {
98
+ it(desc, async () => {
99
+ if (setup) setup()
100
+ const code = await callHook(params)
101
+ check(code)
102
+ })
103
+ }
104
+ })
105
+
106
+ describe('DESTPORT type', () => {
107
+ it('stores local.port as an integer (587/465 auth check)', async () => {
108
+ this.connection.remote.ip = '127.0.0.1'
109
+ await new Promise((resolve) => {
110
+ this.plugin.hook_unrecognized_command(() => resolve(), this.connection, [
111
+ 'XCLIENT',
112
+ 'ADDR=1.2.3.4 DESTPORT=587',
113
+ ])
114
+ })
115
+ assert.strictEqual(this.connection.local.port, 587)
116
+ assert.equal(typeof this.connection.local.port, 'number')
117
+ })
118
+ })
119
+ })