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,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,198 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert/strict')
4
+ const { beforeEach, describe, it } = require('node:test')
5
+
6
+ const { makeConnection, makePlugin } = require('haraka-test-fixtures')
7
+ require('haraka-constants').import(global)
8
+
9
+ describe('toobusy', () => {
10
+ let plugin
11
+
12
+ beforeEach(() => {
13
+ plugin = makePlugin('toobusy', { register: false })
14
+ })
15
+
16
+ describe('register', () => {
17
+ it('registers connect hook with correct priority', () => {
18
+ const hooks = []
19
+ plugin.register_hook = function (hook, name, priority) {
20
+ hooks.push({ hook, name, priority })
21
+ }
22
+
23
+ plugin.register()
24
+
25
+ assert.equal(hooks.length, 1, 'should register one hook')
26
+ assert.equal(hooks[0].hook, 'connect')
27
+ assert.equal(hooks[0].name, 'check_busy')
28
+ assert.equal(hooks[0].priority, -100)
29
+ })
30
+
31
+ it('loads config on register', () => {
32
+ let loadConfigCalled = false
33
+ const originalLoadConfig = plugin.loadConfig
34
+ plugin.loadConfig = function () {
35
+ loadConfigCalled = true
36
+ return originalLoadConfig.call(this)
37
+ }
38
+
39
+ plugin.register()
40
+
41
+ assert.equal(loadConfigCalled, true, 'loadConfig should be called')
42
+ })
43
+
44
+ it('handles missing toobusy-js gracefully', () => {
45
+ assert.doesNotThrow(() => plugin.register())
46
+ })
47
+ })
48
+
49
+ describe('loadConfig', () => {
50
+ beforeEach(() => {
51
+ plugin.register()
52
+ })
53
+
54
+ it('gets toobusy.maxlag config value', () => {
55
+ let configArgs = []
56
+
57
+ plugin.config.get = function (key, type, callback) {
58
+ configArgs = [key, type]
59
+ return '70'
60
+ }
61
+
62
+ plugin.loadConfig()
63
+
64
+ assert.equal(configArgs[0], 'toobusy.maxlag')
65
+ assert.equal(configArgs[1], 'value')
66
+ })
67
+
68
+ it('passes callback to config.get for hot reload', () => {
69
+ let callbackProvided = false
70
+
71
+ plugin.config.get = function (key, type, callback) {
72
+ callbackProvided = typeof callback === 'function'
73
+ return '70'
74
+ }
75
+
76
+ plugin.loadConfig()
77
+
78
+ assert.equal(callbackProvided, true, 'callback should be provided for hot reload')
79
+ })
80
+
81
+ it('handles zero maxLag value', () => {
82
+ plugin.config.get = () => '0'
83
+
84
+ assert.doesNotThrow(() => {
85
+ plugin.loadConfig()
86
+ })
87
+ })
88
+
89
+ it('handles non-numeric maxLag value', () => {
90
+ plugin.config.get = () => 'notanumber'
91
+
92
+ assert.doesNotThrow(() => {
93
+ plugin.loadConfig()
94
+ })
95
+ })
96
+
97
+ it('handles empty string maxLag value', () => {
98
+ plugin.config.get = () => ''
99
+
100
+ assert.doesNotThrow(() => {
101
+ plugin.loadConfig()
102
+ })
103
+ })
104
+
105
+ it('parses numeric maxLag as integer', () => {
106
+ plugin.config.get = () => '100'
107
+
108
+ assert.doesNotThrow(() => {
109
+ plugin.loadConfig()
110
+ })
111
+ })
112
+
113
+ it('supports reload via callback', () => {
114
+ let callbackFn = null
115
+
116
+ plugin.config.get = function (key, type, callback) {
117
+ callbackFn = callback
118
+ return '70'
119
+ }
120
+
121
+ plugin.loadConfig()
122
+
123
+ assert.equal(typeof callbackFn, 'function', 'callback should be provided')
124
+ assert.doesNotThrow(() => {
125
+ if (callbackFn) callbackFn()
126
+ })
127
+ })
128
+ })
129
+
130
+ describe('check_busy', () => {
131
+ beforeEach(() => {
132
+ plugin.register()
133
+ })
134
+
135
+ it('calls next without args when not busy', (t, done) => {
136
+ plugin.config.get = () => '70'
137
+ plugin.loadConfig()
138
+
139
+ plugin.check_busy(function (...args) {
140
+ assert.equal(args.length, 0, 'should call next with no arguments')
141
+ done()
142
+ })
143
+ })
144
+
145
+ it('initializes was_busy state', (t, done) => {
146
+ plugin.config.get = () => '70'
147
+ plugin.loadConfig()
148
+
149
+ plugin.check_busy(function () {
150
+ done()
151
+ })
152
+ })
153
+
154
+ it('is a callable function', () => {
155
+ assert.equal(typeof plugin.check_busy, 'function')
156
+ })
157
+
158
+ it('does not log when not busy', (t, done) => {
159
+ plugin.config.get = () => '70'
160
+ plugin.loadConfig()
161
+
162
+ let logCount = 0
163
+ plugin.logcrit = function () {
164
+ logCount++
165
+ }
166
+
167
+ plugin.check_busy(function () {
168
+ plugin.check_busy(function () {
169
+ assert.equal(logCount, 0, 'should not log when not busy')
170
+ done()
171
+ })
172
+ })
173
+ })
174
+
175
+ it('accepts next callback', (t, done) => {
176
+ plugin.config.get = () => '70'
177
+ plugin.loadConfig()
178
+
179
+ const nextFn = function () {
180
+ done()
181
+ }
182
+
183
+ assert.doesNotThrow(() => {
184
+ plugin.check_busy(nextFn)
185
+ })
186
+ })
187
+
188
+ it('works with connection context', (t, done) => {
189
+ plugin.config.get = () => '70'
190
+ plugin.loadConfig()
191
+
192
+ const conn = makeConnection()
193
+ plugin.check_busy.call(conn, function () {
194
+ done()
195
+ })
196
+ })
197
+ })
198
+ })
@@ -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
+ })