Haraka 3.1.3 → 3.1.5

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 (65) hide show
  1. package/.prettierignore +2 -0
  2. package/CONTRIBUTORS.md +23 -1
  3. package/Changes.md +52 -0
  4. package/Plugins.md +81 -64
  5. package/README.md +1 -1
  6. package/bin/haraka +7 -5
  7. package/connection.js +15 -19
  8. package/docs/Plugins.md +1 -1
  9. package/docs/plugins/aliases.md +0 -2
  10. package/docs/plugins/queue/qmail-queue.md +0 -1
  11. package/logger.js +2 -2
  12. package/outbound/hmail.js +76 -83
  13. package/outbound/index.js +36 -34
  14. package/outbound/queue.js +231 -176
  15. package/package.json +26 -29
  16. package/plugins/prevent_credential_leaks.js +2 -2
  17. package/plugins/process_title.js +1 -1
  18. package/plugins/queue/smtp_forward.js +5 -5
  19. package/plugins/status.js +8 -5
  20. package/plugins/tls.js +1 -1
  21. package/plugins.js +19 -14
  22. package/rfc1869.js +10 -10
  23. package/run_tests +8 -2
  24. package/server.js +15 -10
  25. package/smtp_client.js +10 -15
  26. package/test/config/tls/haraka.local.pem +47 -47
  27. package/test/connection.js +286 -147
  28. package/test/endpoint.js +5 -4
  29. package/test/fixtures/line_socket.js +1 -0
  30. package/test/fixtures/util_hmailitem.js +1 -1
  31. package/test/host_pool.js +57 -31
  32. package/test/logger.js +75 -135
  33. package/test/outbound/bounce_net_errors.js +132 -0
  34. package/test/outbound/bounce_rfc3464.js +226 -0
  35. package/test/outbound/hmail.js +140 -104
  36. package/test/outbound/index.js +61 -101
  37. package/test/outbound/qfile.js +25 -25
  38. package/test/outbound/queue.js +233 -0
  39. package/test/plugins/auth/auth_base.js +39 -44
  40. package/test/plugins/auth/auth_vpopmaild.js +8 -9
  41. package/test/plugins/queue/smtp_forward.js +953 -183
  42. package/test/plugins/rcpt_to.host_list_base.js +58 -93
  43. package/test/plugins/rcpt_to.in_host_list.js +126 -175
  44. package/test/plugins/record_envelope_addresses.js +93 -0
  45. package/test/plugins/status.js +10 -10
  46. package/test/plugins/tls.js +11 -21
  47. package/test/plugins/xclient.js +102 -0
  48. package/test/plugins.js +10 -13
  49. package/test/rfc1869.js +71 -48
  50. package/test/server.js +281 -436
  51. package/test/smtp_client.js +1194 -220
  52. package/test/tls_socket.js +74 -243
  53. package/test/transaction.js +486 -201
  54. package/tls_socket.js +19 -23
  55. package/transaction.js +33 -10
  56. package/config/rabbitmq.ini +0 -10
  57. package/config/rabbitmq_amqplib.ini +0 -19
  58. package/docs/plugins/queue/rabbitmq.md +0 -34
  59. package/docs/plugins/queue/rabbitmq_amqplib.md +0 -51
  60. package/plugins/queue/rabbitmq.js +0 -141
  61. package/plugins/queue/rabbitmq_amqplib.js +0 -96
  62. package/test/config/tls/ec.pem +0 -23
  63. package/test/config/tls/mismatched.pem +0 -49
  64. package/test/outbound_bounce_net_errors.js +0 -157
  65. package/test/outbound_bounce_rfc3464.js +0 -366
@@ -1,8 +1,9 @@
1
1
  'use strict'
2
2
 
3
+ const { describe, it, beforeEach, afterEach } = require('node:test')
3
4
  const assert = require('node:assert')
4
- const fs = require('fs')
5
- const path = require('path')
5
+ const fs = require('node:fs')
6
+ const path = require('node:path')
6
7
 
7
8
  const constants = require('haraka-constants')
8
9
  const logger = require('../../logger')
@@ -22,56 +23,51 @@ describe('outbound', () => {
22
23
  let contents = lines.join(ending)
23
24
  let result = ''
24
25
 
25
- // Set data_lines to lines in contents
26
26
  let match
27
27
  const re = /^([^\n]*\n?)/
28
28
  while ((match = re.exec(contents))) {
29
29
  let line = match[1]
30
- line = line.replace(/\r?\n?$/, '\r\n') // assure \r\n ending
30
+ line = line.replace(/\r?\n?$/, '\r\n')
31
31
  result += line
32
- contents = contents.substr(match[1].length)
33
- if (contents.length === 0) {
34
- break
35
- }
32
+ contents = contents.substring(match[1].length)
33
+ if (contents.length === 0) break
36
34
  }
37
35
 
38
36
  assert.deepEqual(lines.join('\r\n'), result)
39
37
  }
40
38
  })
41
39
 
42
- it('log_methods added', () => {
40
+ it('log_methods added to HMailItem prototype', () => {
43
41
  const levels = ['DATA', 'PROTOCOL', 'DEBUG', 'INFO', 'NOTICE', 'WARN', 'ERROR', 'CRIT', 'ALERT', 'EMERG']
44
-
45
- const HMailItem = require('../../outbound/hmail')
46
-
42
+ // Load via outbound/index to avoid circular-dep boot-order issue
43
+ const HMailItem = require('../../outbound').HMailItem
47
44
  for (const level of levels) {
48
- assert.ok(HMailItem.prototype[`log${level.toLowerCase()}`], `Log method for level: ${level}`)
45
+ assert.ok(HMailItem.prototype[`log${level.toLowerCase()}`], `log method for ${level}`)
49
46
  }
50
47
  })
51
48
 
52
49
  it('set_temp_fail_intervals coverage', () => {
53
50
  const config = require('../../outbound/config')
54
- // Test default configuration
55
51
  assert.deepEqual(
56
52
  config.cfg.temp_fail_intervals,
57
53
  [64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072],
58
54
  )
59
- // Test a simple configuration
55
+
60
56
  config.cfg.temp_fail_intervals = '10s, 1m*2'
61
57
  config.set_temp_fail_intervals()
62
58
  assert.deepEqual(config.cfg.temp_fail_intervals, [10, 60, 60])
63
- // Test a complex configuration
59
+
64
60
  config.cfg.temp_fail_intervals = '30s, 1m, 5m, 9m, 15m*3, 30m*2, 1h*3, 2h*3, 1d'
65
61
  config.set_temp_fail_intervals()
66
62
  assert.deepEqual(
67
63
  config.cfg.temp_fail_intervals,
68
64
  [30, 60, 300, 540, 900, 900, 900, 1800, 1800, 3600, 3600, 3600, 7200, 7200, 7200, 86400],
69
65
  )
70
- // Test the "none" configuration
66
+
71
67
  config.cfg.temp_fail_intervals = 'none'
72
68
  config.set_temp_fail_intervals()
73
69
  assert.deepEqual(config.cfg.temp_fail_intervals, [])
74
- // Test bad config (should revert to default)
70
+
75
71
  config.cfg.temp_fail_intervals = '60 min'
76
72
  config.set_temp_fail_intervals()
77
73
  assert.deepEqual(
@@ -81,29 +77,26 @@ describe('outbound', () => {
81
77
  })
82
78
 
83
79
  describe('get_tls_options', () => {
84
- beforeEach((done) => {
80
+ let outbound, obtls
81
+
82
+ beforeEach(async () => {
85
83
  process.env.HARAKA_TEST_DIR = path.resolve('test')
86
- this.outbound = require('../../outbound')
87
- this.obtls = require('../../outbound/tls')
84
+ outbound = require('../../outbound')
85
+ obtls = require('../../outbound/tls')
88
86
  const tls_socket = require('../../tls_socket')
89
87
 
90
- // reset config to load from tests directory
91
88
  const testDir = path.resolve('test')
92
- this.outbound.config = this.outbound.config.module_config(testDir)
93
- this.obtls.test_config(tls_socket.config.module_config(testDir), this.outbound.config)
94
- this.obtls.init(done)
89
+ outbound.config = outbound.config.module_config(testDir)
90
+ obtls.test_config(tls_socket.config.module_config(testDir), outbound.config)
91
+ await new Promise((resolve) => obtls.init(resolve))
95
92
  })
96
93
 
97
- afterEach((done) => {
94
+ afterEach(() => {
98
95
  delete process.env.HARAKA_TEST_DIR
99
- done()
100
96
  })
101
97
 
102
98
  it('gets TLS properties from tls.ini.outbound', () => {
103
- const tls_config = this.obtls.get_tls_options({
104
- exchange: 'mail.example.com',
105
- })
106
-
99
+ const tls_config = obtls.get_tls_options({ exchange: 'mail.example.com' })
107
100
  assert.deepEqual(tls_config, {
108
101
  servername: 'mail.example.com',
109
102
  key: fs.readFileSync(path.resolve('test', 'config', 'outbound_tls_key.pem')),
@@ -122,116 +115,83 @@ describe('outbound', () => {
122
115
  })
123
116
 
124
117
  describe('build_todo', () => {
125
- beforeEach((done) => {
126
- this.outbound = require('../../outbound')
118
+ let outbound
119
+
120
+ beforeEach(() => {
121
+ outbound = require('../../outbound')
127
122
  try {
128
123
  fs.unlinkSync('test/queue/multibyte')
129
124
  fs.unlinkSync('test/queue/plain')
130
125
  } catch (ignore) {}
131
- done()
132
126
  })
133
127
 
134
- it('saves a file', () => {
128
+ it('saves a plain queue file', () => {
135
129
  const todo = JSON.parse(
136
130
  '{"queue_time":1507509981169,"domain":"redacteed.com","rcpt_to":[{"original":"<postmaster@redacteed.com>","original_host":"redacteed.com","host":"redacteed.com","user":"postmaster"}],"mail_from":{"original":"<matt@tnpi.net>","original_host":"tnpi.net","host":"tnpi.net","user":"matt"},"notes":{"authentication_results":["spf=pass smtp.mailfrom=tnpi.net"],"spf_mail_result":"Pass","spf_mail_record":"v=spf1 a mx include:mx.theartfarm.com ?include:forwards._spf.tnpi.net include:lists._spf.tnpi.net -all","attachment_count":0,"attachments":[{"ctype":"application/pdf","filename":"FileWithoutAccent Chars.pdf","extension":".pdf","md5":"6c1d5f5c047cff3f6320b1210970bdf6"}],"attachment_ctypes":["application/pdf","multipart/mixed","text/plain","application/pdf"],"attachment_files":["FileWithoutaccent Chars.pdf"],"attachment_archive_files":[]},"uuid":"1D5483B0-3E00-4280-A961-3AFD2017B4FC.1"}',
137
131
  )
138
132
  const fd = fs.openSync('test/queue/plain', 'w')
139
- const ws = new fs.createWriteStream('test/queue/plain', {
140
- fd,
141
- flags: constants.WRITE_EXCL,
142
- })
143
- ws.on('close', () => {
144
- // console.log(arguments);
145
- assert.ok(1)
146
- })
147
- ws.on('error', (e) => {
148
- console.error(e)
149
- })
150
- this.outbound.build_todo(todo, ws, () => {
133
+ const ws = new fs.createWriteStream('test/queue/plain', { fd, flags: constants.WRITE_EXCL })
134
+ ws.on('error', (e) => console.error(e))
135
+ outbound.build_todo(todo, ws, () => {
151
136
  ws.write(Buffer.from('This is the message body'))
152
- fs.fsync(fd, () => {
153
- ws.close()
154
- })
137
+ fs.fsync(fd, () => ws.close())
155
138
  })
139
+ assert.ok(true)
156
140
  })
157
141
 
158
- it('saves a file with multibyte chars', () => {
142
+ it('saves a queue file with multibyte chars', () => {
159
143
  const todo = JSON.parse(
160
- '{"queue_time":1507509981169,"domain":"redacteed.com","rcpt_to":[{"original":"<postmaster@redacteed.com>","original_host":"redacteed.com","host":"redacteed.com","user":"postmaster"}],"mail_from":{"original":"<matt@tnpi.net>","original_host":"tnpi.net","host":"tnpi.net","user":"matt"},"notes":{"authentication_results":["spf=pass smtp.mailfrom=tnpi.net"],"spf_mail_result":"Pass","spf_mail_record":"v=spf1 a mx include:mx.theartfarm.com ?include:forwards._spf.tnpi.net include:lists._spf.tnpi.net -all","attachment_count":0,"attachments":[{"ctype":"application/pdf","filename":"FileWîthÁccent Chars.pdf","extension":".pdf","md5":"6c1d5f5c047cff3f6320b1210970bdf6"}],"attachment_ctypes":["application/pdf","multipart/mixed","text/plain","application/pdf"],"attachment_files":["FileWîthÁccent Chars.pdf"],"attachment_archive_files":[]},"uuid":"1D5483B0-3E00-4280-A961-3AFD2017B4FC.1"}',
144
+ '{"queue_time":1507509981169,"domain":"redacteed.com","rcpt_to":[{"original":"<postmaster@redacteed.com>","original_host":"redacteed.com","host":"redacteed.com","user":"postmaster"}],"mail_from":{"original":"<matt@tnpi.net>","original_host":"tnpi.net","host":"tnpi.net","user":"matt"},"notes":{"authentication_results":["spf=pass smtp.mailfrom=tnpi.net"],"spf_mail_result":"Pass","spf_mail_record":"v=spf1 a mx include:mx.theartfarm.com ?include:forwards._spf.tnpi.net include:lists._spf.tnpi.net -all","attachment_count":0,"attachments":[{"ctype":"application/pdf","filename":"FileW\\u00eeth\\u00c1ccent Chars.pdf","extension":".pdf","md5":"6c1d5f5c047cff3f6320b1210970bdf6"}],"attachment_ctypes":["application/pdf","multipart/mixed","text/plain","application/pdf"],"attachment_files":["FileW\\u00eeth\\u00c1ccent Chars.pdf"],"attachment_archive_files":[]},"uuid":"1D5483B0-3E00-4280-A961-3AFD2017B4FC.1"}',
161
145
  )
162
146
  const fd = fs.openSync('test/queue/multibyte', 'w')
163
- const ws = new fs.WriteStream('test/queue/multibyte', {
164
- fd,
165
- flags: constants.WRITE_EXCL,
166
- })
167
- ws.on('close', () => {
168
- assert.ok(1)
169
- })
170
- ws.on('error', (e) => {
171
- console.error(e)
172
- })
173
- this.outbound.build_todo(todo, ws, () => {
147
+ const ws = new fs.WriteStream('test/queue/multibyte', { fd, flags: constants.WRITE_EXCL })
148
+ ws.on('error', (e) => console.error(e))
149
+ outbound.build_todo(todo, ws, () => {
174
150
  ws.write(Buffer.from('This is the message body'))
175
- fs.fsync(fd, () => {
176
- ws.close()
177
- })
151
+ fs.fsync(fd, () => ws.close())
178
152
  })
153
+ assert.ok(true)
179
154
  })
180
155
  })
181
156
 
182
157
  describe('timer_queue', () => {
183
- beforeEach((done) => {
158
+ let outbound, ob_timer_queue
159
+
160
+ beforeEach(() => {
184
161
  process.env.HARAKA_TEST_DIR = path.resolve('test')
185
- this.outbound = require('../../outbound')
162
+ outbound = require('../../outbound')
186
163
  const TimerQueue = require('../../outbound/timer_queue')
187
- this.ob_timer_queue = new TimerQueue(500)
188
- done()
164
+ ob_timer_queue = new TimerQueue(500)
189
165
  })
190
166
 
191
- afterEach((done) => {
167
+ afterEach(() => {
192
168
  delete process.env.HARAKA_TEST_DIR
193
- this.ob_timer_queue.shutdown()
194
- done()
169
+ ob_timer_queue.shutdown()
195
170
  })
196
171
 
197
172
  it('has initial length of 0', () => {
198
- assert.equal(this.ob_timer_queue.length(), 0)
173
+ assert.equal(ob_timer_queue.length(), 0)
199
174
  })
200
175
 
201
176
  it('can add items', () => {
202
- this.ob_timer_queue.add('1', 1000)
203
- this.ob_timer_queue.add('2', 2000)
204
-
205
- assert.equal(this.ob_timer_queue.length(), 2)
177
+ ob_timer_queue.add('1', 1000)
178
+ ob_timer_queue.add('2', 2000)
179
+ assert.equal(ob_timer_queue.length(), 2)
206
180
  })
207
181
 
208
182
  it('can drain items', () => {
209
- this.ob_timer_queue.add('1', 1000)
210
- this.ob_timer_queue.add('2', 2000)
211
-
212
- let tq_length = this.ob_timer_queue.length()
213
-
214
- assert.equal(tq_length, 2)
215
-
216
- this.ob_timer_queue.drain()
217
- tq_length = this.ob_timer_queue.length()
218
-
219
- assert.equal(tq_length, 0)
183
+ ob_timer_queue.add('1', 1000)
184
+ ob_timer_queue.add('2', 2000)
185
+ ob_timer_queue.drain()
186
+ assert.equal(ob_timer_queue.length(), 0)
220
187
  })
221
188
 
222
189
  it('can discard items by id', () => {
223
- this.ob_timer_queue.add('1', 1000)
224
- this.ob_timer_queue.add('2', 2000)
225
-
226
- let tq_length = this.ob_timer_queue.length()
227
-
228
- assert.equal(tq_length, 2)
229
-
230
- this.ob_timer_queue.discard('2')
231
- tq_length = this.ob_timer_queue.length()
232
-
233
- assert.equal(tq_length, 1)
234
- assert.equal(this.ob_timer_queue.queue[0].id, '1')
190
+ ob_timer_queue.add('1', 1000)
191
+ ob_timer_queue.add('2', 2000)
192
+ ob_timer_queue.discard('2')
193
+ assert.equal(ob_timer_queue.length(), 1)
194
+ assert.equal(ob_timer_queue.queue[0].id, '1')
235
195
  })
236
196
  })
237
197
  })
@@ -1,15 +1,15 @@
1
+ 'use strict'
2
+
3
+ const { describe, it, beforeEach } = require('node:test')
1
4
  const assert = require('node:assert')
2
5
  const os = require('node:os')
3
6
 
4
- describe('qfile', () => {
5
- describe('qfile', () => {
6
- beforeEach((done) => {
7
- this.qfile = require('../../outbound/qfile')
8
- done()
9
- })
7
+ const qfile = require('../../outbound/qfile')
10
8
 
9
+ describe('outbound/qfile', () => {
10
+ describe('name', () => {
11
11
  it('name() basic functions', () => {
12
- const name = this.qfile.name()
12
+ const name = qfile.name()
13
13
  const split = name.split('_')
14
14
  assert.equal(split.length, 7)
15
15
  assert.equal(split[2], 0)
@@ -25,7 +25,7 @@ describe('qfile', () => {
25
25
  uid: 'XXYYZZ',
26
26
  host: os.hostname(),
27
27
  }
28
- const name = this.qfile.name(overrides)
28
+ const name = qfile.name(overrides)
29
29
  const split = name.split('_')
30
30
  assert.equal(split.length, 7)
31
31
  assert.equal(split[0], overrides.arrival)
@@ -38,9 +38,9 @@ describe('qfile', () => {
38
38
 
39
39
  it('rnd_unique() is unique-ish', () => {
40
40
  const repeats = 1000
41
- const u = this.qfile.rnd_unique()
41
+ const u = qfile.rnd_unique()
42
42
  for (let i = 0; i < repeats; i++) {
43
- assert.notEqual(u, this.qfile.rnd_unique())
43
+ assert.notEqual(u, qfile.rnd_unique())
44
44
  }
45
45
  })
46
46
  })
@@ -49,7 +49,7 @@ describe('qfile', () => {
49
49
  it('parts() updates previous queue filenames', () => {
50
50
  // $nextattempt_$attempts_$pid_$uniq.$host
51
51
  const name = '1111_0_2222_3333.foo.example.com'
52
- const parts = this.qfile.parts(name)
52
+ const parts = qfile.parts(name)
53
53
  assert.equal(parts.next_attempt, 1111)
54
54
  assert.equal(parts.attempts, 0)
55
55
  assert.equal(parts.pid, 2222)
@@ -65,8 +65,8 @@ describe('qfile', () => {
65
65
  uid: 'XXYYZZ',
66
66
  host: os.hostname(),
67
67
  }
68
- const name = this.qfile.name(overrides)
69
- const parts = this.qfile.parts(name)
68
+ const name = qfile.name(overrides)
69
+ const parts = qfile.parts(name)
70
70
  assert.equal(parts.arrival, overrides.arrival)
71
71
  assert.equal(parts.next_attempt, overrides.next_attempt)
72
72
  assert.equal(parts.attempts, overrides.attempts)
@@ -75,8 +75,8 @@ describe('qfile', () => {
75
75
  assert.equal(parts.host, overrides.host)
76
76
  })
77
77
 
78
- it('handles 4', () => {
79
- const r = this.qfile.parts('1484878079415_0_12345_8888.mta1.example.com')
78
+ it('handles 4-part legacy filename', () => {
79
+ const r = qfile.parts('1484878079415_0_12345_8888.mta1.example.com')
80
80
  delete r.arrival
81
81
  delete r.uid
82
82
  delete r.counter
@@ -89,8 +89,8 @@ describe('qfile', () => {
89
89
  })
90
90
  })
91
91
 
92
- it('handles 7', () => {
93
- const r = this.qfile.parts('1516650518128_1516667073032_8_29538_TkPZWz_1_haraka')
92
+ it('handles 7-part standard filename', () => {
93
+ const r = qfile.parts('1516650518128_1516667073032_8_29538_TkPZWz_1_haraka')
94
94
  delete r.age
95
95
  assert.deepEqual(r, {
96
96
  arrival: 1516650518128,
@@ -103,22 +103,22 @@ describe('qfile', () => {
103
103
  })
104
104
  })
105
105
 
106
- it('punts on 5', () => {
107
- assert.deepEqual(this.qfile.parts('1516650518128_1516667073032_8_29538_TkPZWz'), null)
106
+ it('punts on 5-part filename', () => {
107
+ assert.deepEqual(qfile.parts('1516650518128_1516667073032_8_29538_TkPZWz'), null)
108
108
  })
109
109
  })
110
110
 
111
111
  describe('hostname', () => {
112
- it('hostname, defaults to os.hostname()', () => {
113
- assert.deepEqual(this.qfile.hostname(), require('os').hostname())
112
+ it('defaults to os.hostname()', () => {
113
+ assert.deepEqual(qfile.hostname(), os.hostname())
114
114
  })
115
115
 
116
- it('hostname, replaces \\ char', () => {
117
- assert.deepEqual(this.qfile.hostname('mt\\a1.exam\\ple.com'), 'mt\\057a1.exam\\057ple.com')
116
+ it('replaces backslash char', () => {
117
+ assert.deepEqual(qfile.hostname('mt\\a1.exam\\ple.com'), 'mt\\057a1.exam\\057ple.com')
118
118
  })
119
119
 
120
- it('hostname, replaces _ char', () => {
121
- assert.deepEqual(this.qfile.hostname('mt_a1.exam_ple.com'), 'mt\\137a1.exam\\137ple.com')
120
+ it('replaces underscore char', () => {
121
+ assert.deepEqual(qfile.hostname('mt_a1.exam_ple.com'), 'mt\\137a1.exam\\137ple.com')
122
122
  })
123
123
  })
124
124
  })
@@ -0,0 +1,233 @@
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 = (_file, _parts) => {
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
+ (_file) => {},
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
+ })