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,124 @@
1
+ 'use strict'
2
+
3
+ const { describe, it } = require('node:test')
4
+ const assert = require('node:assert')
5
+ const os = require('node:os')
6
+
7
+ const qfile = require('../../outbound/qfile')
8
+
9
+ describe('outbound/qfile', () => {
10
+ describe('name', () => {
11
+ it('name() basic functions', () => {
12
+ const name = qfile.name()
13
+ const split = name.split('_')
14
+ assert.equal(split.length, 7)
15
+ assert.equal(split[2], 0)
16
+ assert.equal(split[3], process.pid)
17
+ })
18
+
19
+ it('name() with overrides', () => {
20
+ const overrides = {
21
+ arrival: 12345,
22
+ next_attempt: 12345,
23
+ attempts: 15,
24
+ pid: process.pid,
25
+ uid: 'XXYYZZ',
26
+ host: os.hostname(),
27
+ }
28
+ const name = qfile.name(overrides)
29
+ const split = name.split('_')
30
+ assert.equal(split.length, 7)
31
+ assert.equal(split[0], overrides.arrival)
32
+ assert.equal(split[1], overrides.next_attempt)
33
+ assert.equal(split[2], overrides.attempts)
34
+ assert.equal(split[3], overrides.pid)
35
+ assert.equal(split[4], overrides.uid)
36
+ assert.equal(split[6], overrides.host)
37
+ })
38
+
39
+ it('rnd_unique() is unique-ish', () => {
40
+ const repeats = 1000
41
+ const u = qfile.rnd_unique()
42
+ for (let i = 0; i < repeats; i++) {
43
+ assert.notEqual(u, qfile.rnd_unique())
44
+ }
45
+ })
46
+ })
47
+
48
+ describe('parts', () => {
49
+ it('parts() updates previous queue filenames', () => {
50
+ // $nextattempt_$attempts_$pid_$uniq.$host
51
+ const name = '1111_0_2222_3333.foo.example.com'
52
+ const parts = qfile.parts(name)
53
+ assert.equal(parts.next_attempt, 1111)
54
+ assert.equal(parts.attempts, 0)
55
+ assert.equal(parts.pid, 2222)
56
+ assert.equal(parts.host, 'foo.example.com')
57
+ })
58
+
59
+ it('parts() handles standard queue filenames', () => {
60
+ const overrides = {
61
+ arrival: 12345,
62
+ next_attempt: 12345,
63
+ attempts: 15,
64
+ pid: process.pid,
65
+ uid: 'XXYYZZ',
66
+ host: os.hostname(),
67
+ }
68
+ const name = qfile.name(overrides)
69
+ const parts = qfile.parts(name)
70
+ assert.equal(parts.arrival, overrides.arrival)
71
+ assert.equal(parts.next_attempt, overrides.next_attempt)
72
+ assert.equal(parts.attempts, overrides.attempts)
73
+ assert.equal(parts.pid, overrides.pid)
74
+ assert.equal(parts.uid, overrides.uid)
75
+ assert.equal(parts.host, overrides.host)
76
+ })
77
+
78
+ it('handles 4-part legacy filename', () => {
79
+ const r = qfile.parts('1484878079415_0_12345_8888.mta1.example.com')
80
+ delete r.arrival
81
+ delete r.uid
82
+ delete r.counter
83
+ assert.deepEqual(r, {
84
+ next_attempt: 1484878079415,
85
+ attempts: 0,
86
+ pid: 12345,
87
+ host: 'mta1.example.com',
88
+ age: 0,
89
+ })
90
+ })
91
+
92
+ it('handles 7-part standard filename', () => {
93
+ const r = qfile.parts('1516650518128_1516667073032_8_29538_TkPZWz_1_haraka')
94
+ delete r.age
95
+ assert.deepEqual(r, {
96
+ arrival: 1516650518128,
97
+ next_attempt: 1516667073032,
98
+ attempts: 8,
99
+ pid: 29538,
100
+ uid: 'TkPZWz',
101
+ counter: 1,
102
+ host: 'haraka',
103
+ })
104
+ })
105
+
106
+ it('punts on 5-part filename', () => {
107
+ assert.deepEqual(qfile.parts('1516650518128_1516667073032_8_29538_TkPZWz'), null)
108
+ })
109
+ })
110
+
111
+ describe('hostname', () => {
112
+ it('defaults to os.hostname()', () => {
113
+ assert.deepEqual(qfile.hostname(), os.hostname())
114
+ })
115
+
116
+ it('replaces backslash char', () => {
117
+ assert.deepEqual(qfile.hostname('mt\\a1.exam\\ple.com'), 'mt\\057a1.exam\\057ple.com')
118
+ })
119
+
120
+ it('replaces underscore char', () => {
121
+ assert.deepEqual(qfile.hostname('mt_a1.exam_ple.com'), 'mt\\137a1.exam\\137ple.com')
122
+ })
123
+ })
124
+ })
@@ -0,0 +1,325 @@
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
+ const os = require('node:os')
8
+
9
+ const queue = require('../../outbound/queue')
10
+ const qfile = require('../../outbound/qfile')
11
+
12
+ const sourceQueueDir = path.join('test', 'queue')
13
+ const testQueueDir = path.join('test', 'test-queue')
14
+ const fixtureFiles = [
15
+ '1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka',
16
+ '1508269674999_1508269674999_0_34002_socVUF_1_haraka',
17
+ ]
18
+
19
+ const clearTestQueue = () => {
20
+ fs.mkdirSync(testQueueDir, { recursive: true })
21
+ for (const file of fs.readdirSync(testQueueDir)) {
22
+ fs.unlinkSync(path.join(testQueueDir, file))
23
+ }
24
+ }
25
+
26
+ const populateTestQueue = () => {
27
+ clearTestQueue()
28
+ for (const file of fixtureFiles) {
29
+ fs.copyFileSync(path.join(sourceQueueDir, file), path.join(testQueueDir, file))
30
+ }
31
+ }
32
+
33
+ describe('outbound/queue', () => {
34
+ describe('read_parts', () => {
35
+ it('parses valid queue filenames', () => {
36
+ const filename = '1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka'
37
+ const parts = queue.read_parts(filename)
38
+ assert.ok(parts)
39
+ assert.equal(parts.arrival, 1507509981169)
40
+ assert.equal(parts.next_attempt, 1507509981169)
41
+ assert.equal(parts.attempts, 0)
42
+ assert.equal(parts.pid, 61403)
43
+ assert.equal(parts.uid, 'e0Y0Ym')
44
+ })
45
+
46
+ it('rejects dot files', () => {
47
+ assert.strictEqual(queue.read_parts('__tmp__.filename'), false)
48
+ })
49
+
50
+ it('rejects error files', () => {
51
+ assert.strictEqual(queue.read_parts('error.something'), false)
52
+ })
53
+
54
+ it('rejects invalid queue files', () => {
55
+ assert.strictEqual(queue.read_parts('invalid-file'), false)
56
+ })
57
+ })
58
+
59
+ describe('load_queue_files', () => {
60
+ beforeEach(() => {
61
+ populateTestQueue()
62
+ })
63
+ afterEach(() => {
64
+ clearTestQueue()
65
+ })
66
+
67
+ it('processes valid queue files', async () => {
68
+ const seen = []
69
+
70
+ const files = await queue.load_queue_files(
71
+ null,
72
+ ['1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka'],
73
+ (file) => {
74
+ seen.push(file)
75
+ return file
76
+ },
77
+ )
78
+ assert.equal(seen.length, 1)
79
+ assert.equal(files[0], '1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka')
80
+ })
81
+
82
+ it('skips invalid files', async () => {
83
+ const seen = []
84
+
85
+ await queue.load_queue_files(
86
+ null,
87
+ ['1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka', 'invalid-file', 'zero-length'],
88
+ (file) => {
89
+ seen.push(file)
90
+ },
91
+ )
92
+ assert.equal(seen.length, 1)
93
+ })
94
+
95
+ it('filters files by pid', async () => {
96
+ let renameAttempts = 0
97
+
98
+ const originalRename = queue.rename_to_actual_pid
99
+ queue.rename_to_actual_pid = () => {
100
+ renameAttempts++
101
+ throw new Error('test skip')
102
+ }
103
+
104
+ await queue.load_queue_files(
105
+ 61403,
106
+ [
107
+ '1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka',
108
+ '1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka',
109
+ ],
110
+ () => {},
111
+ )
112
+ queue.rename_to_actual_pid = originalRename
113
+ assert.equal(renameAttempts, 1)
114
+ })
115
+ })
116
+
117
+ describe('ensure_queue_dir', () => {
118
+ it('creates queue dir', async () => {
119
+ const tmpDir = path.join(os.tmpdir(), `haraka-test-queue-${Date.now()}`)
120
+
121
+ const originalQueueDir = queue.queue_dir
122
+ queue.queue_dir = tmpDir
123
+
124
+ try {
125
+ await queue.ensure_queue_dir()
126
+ assert.ok(fs.existsSync(tmpDir))
127
+ const stat = await fs.promises.stat(tmpDir)
128
+ assert.ok(stat.isDirectory())
129
+ } catch (err) {
130
+ assert.fail(`ensure_queue_dir threw an error: ${err.message}`)
131
+ } finally {
132
+ queue.queue_dir = originalQueueDir
133
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true })
134
+ }
135
+ })
136
+
137
+ it('returns early if queue dir already exists', async () => {
138
+ const tmpDir = path.join(os.tmpdir(), `haraka-test-queue-exists-${Date.now()}`)
139
+ fs.mkdirSync(tmpDir)
140
+
141
+ const originalQueueDir = queue.queue_dir
142
+ queue.queue_dir = tmpDir
143
+
144
+ try {
145
+ await queue.ensure_queue_dir()
146
+ assert.ok(fs.existsSync(tmpDir))
147
+ } catch (err) {
148
+ assert.fail(`ensure_queue_dir threw an error: ${err.message}`)
149
+ } finally {
150
+ queue.queue_dir = originalQueueDir
151
+ fs.rmSync(tmpDir, { recursive: true })
152
+ }
153
+ })
154
+ })
155
+
156
+ describe('_load_cur_queue', () => {
157
+ beforeEach(() => {
158
+ populateTestQueue()
159
+ })
160
+ afterEach(() => {
161
+ clearTestQueue()
162
+ })
163
+
164
+ it('reads queue directory and processes files', async () => {
165
+ const processedFiles = []
166
+ await queue._load_cur_queue(null, (file) => {
167
+ processedFiles.push(file)
168
+ })
169
+
170
+ assert.ok(processedFiles.length >= 0)
171
+ })
172
+ })
173
+
174
+ describe('list_queue', () => {
175
+ beforeEach(() => {
176
+ populateTestQueue()
177
+ })
178
+ afterEach(() => {
179
+ clearTestQueue()
180
+ })
181
+
182
+ it('returns todo objects from real queue files', async () => {
183
+ const qlist = await queue.list_queue()
184
+ assert.ok(Array.isArray(qlist))
185
+ assert.ok(qlist.length > 0)
186
+ assert.ok(qlist[0].mail_from)
187
+ assert.ok(Array.isArray(qlist[0].rcpt_to))
188
+ })
189
+ })
190
+
191
+ describe('stat_queue', () => {
192
+ beforeEach(() => {
193
+ populateTestQueue()
194
+ })
195
+ afterEach(() => {
196
+ clearTestQueue()
197
+ })
198
+
199
+ it('returns queue stats', async () => {
200
+ const stats = await queue.stat_queue()
201
+ assert.ok(stats)
202
+ assert.ok('queue_dir' in stats)
203
+ assert.ok(stats.queue_count >= 1)
204
+ })
205
+ })
206
+
207
+ describe('load_pid_queue', () => {
208
+ beforeEach(() => {
209
+ populateTestQueue()
210
+ })
211
+ afterEach(() => {
212
+ clearTestQueue()
213
+ })
214
+
215
+ it('delegates pid loading to init_queue', async () => {
216
+ const parts = qfile.parts(fixtureFiles[0])
217
+ const observed = []
218
+ const originalLoadQueue = queue.init_queue
219
+
220
+ queue.init_queue = (pid) => {
221
+ observed.push(pid)
222
+ }
223
+
224
+ try {
225
+ assert.ok(fs.existsSync(path.join(testQueueDir, fixtureFiles[0])))
226
+ await queue.load_pid_queue(parts.pid)
227
+ assert.deepEqual(observed, [parts.pid])
228
+ } finally {
229
+ queue.init_queue = originalLoadQueue
230
+ }
231
+ })
232
+ })
233
+
234
+ describe('queue maintenance', () => {
235
+ it('delete_dot_files removes leftover dot files only', async () => {
236
+ const tmpDir = path.join(os.tmpdir(), `haraka-dot-clean-${Date.now()}`)
237
+ fs.mkdirSync(tmpDir, { recursive: true })
238
+ const dotName = `${qfile.platformDOT}leftover`
239
+ const normalName = 'keep-me'
240
+ fs.writeFileSync(path.join(tmpDir, dotName), 'x')
241
+ fs.writeFileSync(path.join(tmpDir, normalName), 'x')
242
+
243
+ const originalQueueDir = queue.queue_dir
244
+ queue.queue_dir = tmpDir
245
+ try {
246
+ await queue.delete_dot_files()
247
+ assert.equal(fs.existsSync(path.join(tmpDir, dotName)), false)
248
+ assert.equal(fs.existsSync(path.join(tmpDir, normalName)), true)
249
+ } finally {
250
+ queue.queue_dir = originalQueueDir
251
+ fs.rmSync(tmpDir, { recursive: true, force: true })
252
+ }
253
+ })
254
+
255
+ it('_add_hmail pushes immediate items and schedules delayed ones', () => {
256
+ const originalPush = queue.delivery_queue.push
257
+ const originalAdd = queue.temp_fail_queue.add
258
+ const pushed = []
259
+ const delayed = []
260
+ let delayedCb
261
+
262
+ queue.delivery_queue.push = (item) => pushed.push(item)
263
+ queue.temp_fail_queue.add = (id, ms, cb) => {
264
+ delayed.push([id, ms])
265
+ delayedCb = cb
266
+ }
267
+ queue.cur_time = new Date()
268
+
269
+ const immediate = { filename: 'a', next_process: queue.cur_time - 1 }
270
+ const future = { filename: 'b', next_process: queue.cur_time.getTime() + 1000 }
271
+
272
+ try {
273
+ queue._add_hmail(immediate)
274
+ queue._add_hmail(future)
275
+ assert.equal(pushed.length, 1)
276
+ assert.equal(delayed.length, 1)
277
+ assert.equal(delayed[0][0], 'b')
278
+ delayedCb()
279
+ assert.equal(pushed.length, 2)
280
+ } finally {
281
+ queue.delivery_queue.push = originalPush
282
+ queue.temp_fail_queue.add = originalAdd
283
+ }
284
+ })
285
+
286
+ it('scan_queue_pids returns unique pids from queue files', async () => {
287
+ populateTestQueue()
288
+ const originalQueueDir = queue.queue_dir
289
+ queue.queue_dir = testQueueDir
290
+
291
+ try {
292
+ const pids = await queue.scan_queue_pids()
293
+ assert.ok(Array.isArray(pids))
294
+ assert.equal(pids.length >= 1, true)
295
+ } finally {
296
+ queue.queue_dir = originalQueueDir
297
+ clearTestQueue()
298
+ }
299
+ })
300
+
301
+ it('scan_queue_pids throws when queue dir cannot be read', async () => {
302
+ const originalQueueDir = queue.queue_dir
303
+ const badPath = path.join(os.tmpdir(), `queue-not-dir-${Date.now()}`)
304
+ fs.writeFileSync(badPath, 'x')
305
+ queue.queue_dir = badPath
306
+ try {
307
+ await assert.rejects(() => queue.scan_queue_pids())
308
+ } finally {
309
+ queue.queue_dir = originalQueueDir
310
+ fs.rmSync(badPath, { force: true })
311
+ }
312
+ })
313
+
314
+ it('delete_dot_files handles readdir errors without throwing', async () => {
315
+ const originalQueueDir = queue.queue_dir
316
+ queue.queue_dir = path.join(os.tmpdir(), `missing-dot-${Date.now()}`)
317
+ try {
318
+ await queue.delete_dot_files()
319
+ assert.ok(true)
320
+ } finally {
321
+ queue.queue_dir = originalQueueDir
322
+ }
323
+ })
324
+ })
325
+ })