Haraka 3.1.3 → 3.1.4

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 (55) hide show
  1. package/.prettierignore +2 -0
  2. package/CONTRIBUTORS.md +23 -1
  3. package/Changes.md +38 -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 +25 -26
  16. package/plugins/prevent_credential_leaks.js +2 -2
  17. package/plugins/process_title.js +1 -1
  18. package/plugins/queue/smtp_forward.js +1 -1
  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 +20 -2
  24. package/server.js +15 -10
  25. package/smtp_client.js +2 -9
  26. package/test/config/tls/haraka.local.pem +47 -47
  27. package/test/connection.js +286 -147
  28. package/test/fixtures/line_socket.js +1 -0
  29. package/test/fixtures/util_hmailitem.js +1 -1
  30. package/test/outbound/bounce_net_errors.js +176 -0
  31. package/test/outbound/bounce_rfc3464.js +303 -0
  32. package/test/outbound/hmail.js +140 -104
  33. package/test/outbound/index.js +61 -101
  34. package/test/outbound/qfile.js +25 -25
  35. package/test/outbound/queue.js +233 -0
  36. package/test/plugins/queue/smtp_forward.js +1 -1
  37. package/test/plugins/record_envelope_addresses.js +93 -0
  38. package/test/plugins/tls.js +2 -2
  39. package/test/plugins/xclient.js +137 -0
  40. package/test/rfc1869.js +43 -0
  41. package/test/smtp_client.js +6 -6
  42. package/test/transaction.js +486 -201
  43. package/tls_socket.js +3 -3
  44. package/transaction.js +33 -10
  45. package/config/rabbitmq.ini +0 -10
  46. package/config/rabbitmq_amqplib.ini +0 -19
  47. package/docs/plugins/queue/rabbitmq.md +0 -34
  48. package/docs/plugins/queue/rabbitmq_amqplib.md +0 -51
  49. package/plugins/queue/rabbitmq.js +0 -141
  50. package/plugins/queue/rabbitmq_amqplib.js +0 -96
  51. package/test/config/tls/ec.pem +0 -23
  52. package/test/config/tls/mismatched.pem +0 -49
  53. package/test/outbound_bounce_net_errors.js +0 -157
  54. package/test/outbound_bounce_rfc3464.js +0 -366
  55. package/test/tls_socket.js +0 -273
@@ -0,0 +1,176 @@
1
+ 'use strict'
2
+
3
+ // Testing bounce email contents related to errors occurring during SMTP dialog.
4
+ // Strategy: create an HMailItem via fixtures, invoke an outbound method, then
5
+ // verify that the correct bounce/temp_fail handler is called.
6
+
7
+ const { describe, it, beforeEach, afterEach } = require('node:test')
8
+ const assert = require('node:assert')
9
+ const dns = require('node:dns')
10
+ const fs = require('node:fs')
11
+ const path = require('node:path')
12
+
13
+ const constants = require('haraka-constants')
14
+
15
+ // Load outbound/index FIRST to avoid the circular-dependency boot-order issue:
16
+ // hmail.js → require('./index') while index.js is still loading causes queue.js
17
+ // to capture a stale (empty) module.exports for hmail.js.
18
+ const outbound = require('../../outbound')
19
+ const HMailItem = outbound.HMailItem
20
+ const TODOItem = require('../../outbound/todo')
21
+
22
+ const util_hmailitem = require('../fixtures/util_hmailitem')
23
+
24
+ const outbound_context = { TODOItem, exports: outbound }
25
+ const queue_dir = path.resolve(__dirname, '../test-queue')
26
+
27
+ // ── Helpers ───────────────────────────────────────────────────────────────────
28
+
29
+ const ensureQueueDir = () =>
30
+ new Promise((resolve, reject) => {
31
+ fs.exists(queue_dir, (exists) => {
32
+ if (exists) return resolve()
33
+ fs.mkdir(queue_dir, (err) => (err ? reject(err) : resolve()))
34
+ })
35
+ })
36
+
37
+ const cleanQueueDir = () =>
38
+ new Promise((resolve, reject) => {
39
+ fs.exists(queue_dir, (exists) => {
40
+ if (!exists) return resolve()
41
+ try {
42
+ for (const file of fs.readdirSync(queue_dir)) {
43
+ const full = path.resolve(queue_dir, file)
44
+ if (fs.lstatSync(full).isDirectory()) return reject(new Error(`unexpected subdirectory: ${full}`))
45
+ fs.unlinkSync(full)
46
+ }
47
+ resolve()
48
+ } catch (err) {
49
+ reject(err)
50
+ }
51
+ })
52
+ })
53
+
54
+ /** Creates a mock HMailItem, resolving with it or rejecting on error. */
55
+ const mockHMailItem = (ctx, opts = {}) =>
56
+ new Promise((resolve, reject) => {
57
+ util_hmailitem.newMockHMailItem(ctx, reject, opts, resolve)
58
+ })
59
+
60
+ // ── Tests ─────────────────────────────────────────────────────────────────────
61
+
62
+ describe('outbound_bounce_net_errors', () => {
63
+ beforeEach(ensureQueueDir)
64
+ afterEach(cleanQueueDir)
65
+
66
+ it('get_mx=DENY triggers bounce with dsn_status 5.1.2', async () => {
67
+ const mock_hmail = await mockHMailItem(outbound_context)
68
+ await new Promise((resolve, reject) => {
69
+ const orig = HMailItem.prototype.bounce
70
+ HMailItem.prototype.bounce = function (err, opts) {
71
+ try {
72
+ assert.equal(this.todo.rcpt_to[0].dsn_status, '5.1.2', 'dsn status')
73
+ resolve()
74
+ } catch (e) {
75
+ reject(e)
76
+ } finally {
77
+ HMailItem.prototype.bounce = orig
78
+ }
79
+ }
80
+ mock_hmail.domain = mock_hmail.todo.domain
81
+ HMailItem.prototype.get_mx_respond.apply(mock_hmail, [constants.deny, {}])
82
+ })
83
+ })
84
+
85
+ it('get_mx=DENYSOFT triggers temp_fail with dsn_status 4.1.2', async () => {
86
+ const mock_hmail = await mockHMailItem(outbound_context)
87
+ await new Promise((resolve, reject) => {
88
+ const orig = HMailItem.prototype.temp_fail
89
+ HMailItem.prototype.temp_fail = function (err, opts) {
90
+ try {
91
+ assert.equal(this.todo.rcpt_to[0].dsn_status, '4.1.2', 'dsn status')
92
+ resolve()
93
+ } catch (e) {
94
+ reject(e)
95
+ } finally {
96
+ HMailItem.prototype.temp_fail = orig
97
+ }
98
+ }
99
+ mock_hmail.domain = mock_hmail.todo.domain
100
+ HMailItem.prototype.get_mx_respond.apply(mock_hmail, [constants.denysoft, {}])
101
+ })
102
+ })
103
+
104
+ it('get_mx_error({code:NXDOMAIN}) triggers bounce with dsn_status 5.1.2', async () => {
105
+ const mock_hmail = await mockHMailItem(outbound_context)
106
+ await new Promise((resolve, reject) => {
107
+ const orig = HMailItem.prototype.bounce
108
+ HMailItem.prototype.bounce = function (err, opts) {
109
+ try {
110
+ assert.equal(this.todo.rcpt_to[0].dsn_status, '5.1.2', 'dsn status')
111
+ resolve()
112
+ } catch (e) {
113
+ reject(e)
114
+ } finally {
115
+ HMailItem.prototype.bounce = orig
116
+ }
117
+ }
118
+ HMailItem.prototype.get_mx_error.apply(mock_hmail, [{ code: dns.NXDOMAIN }])
119
+ })
120
+ })
121
+
122
+ it("get_mx_error({code:'SOME-OTHER-ERR'}) triggers temp_fail with dsn_status 4.1.0", async () => {
123
+ const mock_hmail = await mockHMailItem(outbound_context)
124
+ await new Promise((resolve, reject) => {
125
+ const orig = HMailItem.prototype.temp_fail
126
+ HMailItem.prototype.temp_fail = function (err, opts) {
127
+ try {
128
+ assert.equal(this.todo.rcpt_to[0].dsn_status, '4.1.0', 'dsn status')
129
+ resolve()
130
+ } catch (e) {
131
+ reject(e)
132
+ } finally {
133
+ HMailItem.prototype.temp_fail = orig
134
+ }
135
+ }
136
+ HMailItem.prototype.get_mx_error.apply(mock_hmail, [{ code: 'SOME-OTHER-ERR' }, {}])
137
+ })
138
+ })
139
+
140
+ it("found_mx(null, [{priority:0,exchange:''}]) triggers bounce with dsn_status 5.1.2", async () => {
141
+ const mock_hmail = await mockHMailItem(outbound_context)
142
+ await new Promise((resolve, reject) => {
143
+ const orig = HMailItem.prototype.bounce
144
+ HMailItem.prototype.bounce = function (err, opts) {
145
+ try {
146
+ assert.equal(this.todo.rcpt_to[0].dsn_status, '5.1.2', 'dsn status')
147
+ resolve()
148
+ } catch (e) {
149
+ reject(e)
150
+ } finally {
151
+ HMailItem.prototype.bounce = orig
152
+ }
153
+ }
154
+ HMailItem.prototype.found_mx.apply(mock_hmail, [[{ priority: 0, exchange: '' }]])
155
+ })
156
+ })
157
+
158
+ it('try_deliver with empty mxlist triggers temp_fail with dsn_status 5.1.2', async () => {
159
+ const mock_hmail = await mockHMailItem(outbound_context)
160
+ mock_hmail.mxlist = []
161
+ await new Promise((resolve, reject) => {
162
+ const orig = HMailItem.prototype.temp_fail
163
+ HMailItem.prototype.temp_fail = function (err, opts) {
164
+ try {
165
+ assert.equal(this.todo.rcpt_to[0].dsn_status, '5.1.2', 'dsn status')
166
+ resolve()
167
+ } catch (e) {
168
+ reject(e)
169
+ } finally {
170
+ HMailItem.prototype.temp_fail = orig
171
+ }
172
+ }
173
+ HMailItem.prototype.try_deliver.apply(mock_hmail, [])
174
+ })
175
+ })
176
+ })
@@ -0,0 +1,303 @@
1
+ 'use strict'
2
+
3
+ // Testing bounce email contents related to errors occurring during SMTP dialog.
4
+ // These tests simulate a remote SMTP server responding with various error codes
5
+ // and verify that Haraka generates correctly formatted RFC 3464 DSN bounce messages.
6
+
7
+ const { describe, it, beforeEach, afterEach } = require('node:test')
8
+ const assert = require('node:assert')
9
+ const fs = require('node:fs')
10
+ const path = require('node:path')
11
+
12
+ // Load outbound/index FIRST to avoid the circular-dependency boot-order issue.
13
+ const outbound = require('../../outbound')
14
+ const HMailItem = outbound.HMailItem
15
+ const TODOItem = require('../../outbound/todo')
16
+ const obc = require('../../outbound/config')
17
+
18
+ const util_hmailitem = require('../fixtures/util_hmailitem')
19
+ const mock_sock = require('../fixtures/line_socket')
20
+
21
+ obc.cfg.pool_concurrency_max = 0
22
+
23
+ const outbound_context = { TODOItem, exports: outbound }
24
+ const queue_dir = path.resolve(__dirname, '../test-queue')
25
+
26
+ // ── Helpers ───────────────────────────────────────────────────────────────────
27
+
28
+ const ensureQueueDir = () =>
29
+ new Promise((resolve, reject) => {
30
+ fs.exists(queue_dir, (exists) => {
31
+ if (exists) return resolve()
32
+ fs.mkdir(queue_dir, (err) => (err ? reject(err) : resolve()))
33
+ })
34
+ })
35
+
36
+ const cleanQueueDir = () =>
37
+ new Promise((resolve, reject) => {
38
+ fs.exists(queue_dir, (exists) => {
39
+ if (!exists) return resolve()
40
+ try {
41
+ for (const file of fs.readdirSync(queue_dir)) {
42
+ const full = path.resolve(queue_dir, file)
43
+ if (fs.lstatSync(full).isDirectory()) return reject(new Error(`unexpected subdirectory: ${full}`))
44
+ fs.unlinkSync(full)
45
+ }
46
+ resolve()
47
+ } catch (err) {
48
+ reject(err)
49
+ }
50
+ })
51
+ })
52
+
53
+ const mockHMailItem = (ctx, opts = {}) =>
54
+ new Promise((resolve, reject) => {
55
+ util_hmailitem.newMockHMailItem(ctx, reject, opts, resolve)
56
+ })
57
+
58
+ // ── Tests ─────────────────────────────────────────────────────────────────────
59
+
60
+ describe('outbound_bounce_rfc3464', () => {
61
+ beforeEach(ensureQueueDir)
62
+ afterEach(cleanQueueDir)
63
+
64
+ it('MAIL FROM 500 triggers RFC3464 bounce with status 5.0.0', async () => {
65
+ const mock_hmail = await mockHMailItem(outbound_context)
66
+ const mock_socket = mock_sock.connect('testhost', 'testport')
67
+ mock_socket.writable = true
68
+
69
+ await new Promise((resolve, reject) => {
70
+ const orig = outbound_context.exports.send_email
71
+ outbound_context.exports.send_email = (from, to, contents, cb, opts) => {
72
+ try {
73
+ assert.match(contents, /^Content-type: message\/delivery-status/m)
74
+ assert.match(contents, /^Final-Recipient: rfc822;recipient@domain/m)
75
+ assert.match(contents, /^Action: failed/m)
76
+ assert.match(contents, /^Status: 5\.0\.0/m)
77
+ assert.match(contents, /Absolutely not acceptable\. Basic Test Only\./)
78
+ resolve()
79
+ } catch (e) {
80
+ reject(e)
81
+ } finally {
82
+ outbound_context.exports.send_email = orig
83
+ }
84
+ }
85
+ util_hmailitem.playTestSmtpConversation(
86
+ mock_hmail,
87
+ mock_socket,
88
+ reject,
89
+ [
90
+ { from: 'remote', line: '220 testing-smtp' },
91
+ { from: 'haraka', test: (l) => l.match(/^EHLO /), description: 'EHLO' },
92
+ { from: 'remote', line: '220-testing-smtp' },
93
+ { from: 'remote', line: '220 8BITMIME' },
94
+ { from: 'haraka', test: 'MAIL FROM:<sender@domain>' },
95
+ { from: 'remote', line: '500 5.0.0 Absolutely not acceptable. Basic Test Only.' },
96
+ { from: 'haraka', test: 'QUIT', end_test: true },
97
+ ],
98
+ () => {},
99
+ )
100
+ })
101
+ })
102
+
103
+ it('early 3XX response triggers temp_fail with status 3.0.0', async () => {
104
+ const mock_hmail = await mockHMailItem(outbound_context)
105
+ const mock_socket = mock_sock.connect('testhost', 'testport')
106
+ mock_socket.writable = true
107
+
108
+ await new Promise((resolve, reject) => {
109
+ const orig = HMailItem.prototype.temp_fail
110
+ HMailItem.prototype.temp_fail = function (err, opts) {
111
+ try {
112
+ assert.equal(this.todo.rcpt_to[0].dsn_status, '3.0.0')
113
+ assert.equal(this.todo.rcpt_to[0].dsn_action, 'delayed')
114
+ assert.match(this.todo.rcpt_to[0].dsn_smtp_response, /No time for you right now/)
115
+ resolve()
116
+ } catch (e) {
117
+ reject(e)
118
+ } finally {
119
+ HMailItem.prototype.temp_fail = orig
120
+ }
121
+ }
122
+ util_hmailitem.playTestSmtpConversation(
123
+ mock_hmail,
124
+ mock_socket,
125
+ reject,
126
+ [
127
+ { from: 'remote', line: '220 testing-smtp' },
128
+ { from: 'haraka', test: (l) => l.match(/^EHLO /), description: 'EHLO' },
129
+ { from: 'remote', line: '220-testing-smtp' },
130
+ { from: 'remote', line: '220 8BITMIME' },
131
+ { from: 'haraka', test: 'MAIL FROM:<sender@domain>' },
132
+ { from: 'remote', line: '300 3.0.0 No time for you right now' },
133
+ { from: 'haraka', test: 'QUIT', end_test: true },
134
+ ],
135
+ () => {},
136
+ )
137
+ })
138
+ })
139
+
140
+ it('RCPT-TO 4XX triggers temp_fail with status 4.0.0', async () => {
141
+ const mock_hmail = await mockHMailItem(outbound_context)
142
+ const mock_socket = mock_sock.connect('testhost', 'testport')
143
+ mock_socket.writable = true
144
+
145
+ await new Promise((resolve, reject) => {
146
+ const orig = HMailItem.prototype.temp_fail
147
+ HMailItem.prototype.temp_fail = function (err, opts) {
148
+ try {
149
+ assert.equal(this.todo.rcpt_to[0].dsn_status, '4.0.0')
150
+ assert.equal(this.todo.rcpt_to[0].dsn_action, 'delayed')
151
+ assert.match(this.todo.rcpt_to[0].dsn_smtp_response, /Currently not available\. Try again later\./)
152
+ resolve()
153
+ } catch (e) {
154
+ reject(e)
155
+ } finally {
156
+ HMailItem.prototype.temp_fail = orig
157
+ }
158
+ }
159
+ util_hmailitem.playTestSmtpConversation(
160
+ mock_hmail,
161
+ mock_socket,
162
+ reject,
163
+ [
164
+ { from: 'remote', line: '220 testing-smtp' },
165
+ { from: 'haraka', test: (l) => l.match(/^EHLO /), description: 'EHLO' },
166
+ { from: 'remote', line: '220-testing-smtp' },
167
+ { from: 'remote', line: '220 8BITMIME' },
168
+ { from: 'haraka', test: 'MAIL FROM:<sender@domain>' },
169
+ { from: 'remote', line: '250 2.1.0 Ok' },
170
+ { from: 'haraka', test: 'RCPT TO:<recipient@domain>' },
171
+ { from: 'remote', line: '400 4.0.0 Currently not available. Try again later.' },
172
+ { from: 'haraka', test: 'QUIT', end_test: true },
173
+ ],
174
+ () => {},
175
+ )
176
+ })
177
+ })
178
+
179
+ it('DATA 4XX triggers temp_fail with status 4.6.0', async () => {
180
+ const mock_hmail = await mockHMailItem(outbound_context)
181
+ const mock_socket = mock_sock.connect('testhost', 'testport')
182
+ mock_socket.writable = true
183
+
184
+ await new Promise((resolve, reject) => {
185
+ const orig = HMailItem.prototype.temp_fail
186
+ HMailItem.prototype.temp_fail = function (err, opts) {
187
+ try {
188
+ assert.equal(this.todo.rcpt_to[0].dsn_status, '4.6.0')
189
+ assert.equal(this.todo.rcpt_to[0].dsn_action, 'delayed')
190
+ assert.match(this.todo.rcpt_to[0].dsn_smtp_response, /Currently I do not like ascii art cats\./)
191
+ resolve()
192
+ } catch (e) {
193
+ reject(e)
194
+ } finally {
195
+ HMailItem.prototype.temp_fail = orig
196
+ }
197
+ }
198
+ util_hmailitem.playTestSmtpConversation(
199
+ mock_hmail,
200
+ mock_socket,
201
+ reject,
202
+ [
203
+ { from: 'remote', line: '220 testing-smtp' },
204
+ { from: 'haraka', test: (l) => l.match(/^EHLO /), description: 'EHLO' },
205
+ { from: 'remote', line: '220-testing-smtp' },
206
+ { from: 'remote', line: '220 8BITMIME' },
207
+ { from: 'haraka', test: 'MAIL FROM:<sender@domain>' },
208
+ { from: 'remote', line: '250 2.1.0 Ok' },
209
+ { from: 'haraka', test: 'RCPT TO:<recipient@domain>' },
210
+ { from: 'remote', line: '250 2.1.5 Ok' },
211
+ { from: 'haraka', test: 'DATA' },
212
+ { from: 'remote', line: '450 4.6.0 Currently I do not like ascii art cats.' },
213
+ { from: 'haraka', test: 'QUIT', end_test: true },
214
+ ],
215
+ () => {},
216
+ )
217
+ })
218
+ })
219
+
220
+ it('RCPT-TO 5XX triggers RFC3464 bounce with status 5.1.1', async () => {
221
+ const mock_hmail = await mockHMailItem(outbound_context)
222
+ const mock_socket = mock_sock.connect('testhost', 'testport')
223
+ mock_socket.writable = true
224
+
225
+ await new Promise((resolve, reject) => {
226
+ const orig = outbound_context.exports.send_email
227
+ outbound_context.exports.send_email = (from, to, contents, cb, opts) => {
228
+ try {
229
+ assert.match(contents, /^Content-type: message\/delivery-status/m)
230
+ assert.match(contents, /^Final-Recipient: rfc822;recipient@domain/m)
231
+ assert.match(contents, /^Action: failed/m)
232
+ assert.match(contents, /^Status: 5\.1\.1/m)
233
+ assert.match(contents, /Not available and will not come back/)
234
+ resolve()
235
+ } catch (e) {
236
+ reject(e)
237
+ } finally {
238
+ outbound_context.exports.send_email = orig
239
+ }
240
+ }
241
+ util_hmailitem.playTestSmtpConversation(
242
+ mock_hmail,
243
+ mock_socket,
244
+ reject,
245
+ [
246
+ { from: 'remote', line: '220 testing-smtp' },
247
+ { from: 'haraka', test: (l) => l.match(/^EHLO /), description: 'EHLO' },
248
+ { from: 'remote', line: '220-testing-smtp' },
249
+ { from: 'remote', line: '220 8BITMIME' },
250
+ { from: 'haraka', test: 'MAIL FROM:<sender@domain>' },
251
+ { from: 'remote', line: '250 2.1.0 Ok' },
252
+ { from: 'haraka', test: 'RCPT TO:<recipient@domain>' },
253
+ { from: 'remote', line: '550 5.1.1 Not available and will not come back' },
254
+ { from: 'haraka', test: 'QUIT', end_test: true },
255
+ ],
256
+ () => {},
257
+ )
258
+ })
259
+ })
260
+
261
+ it('DATA 5XX triggers RFC3464 bounce with status 5.6.0', async () => {
262
+ const mock_hmail = await mockHMailItem(outbound_context)
263
+ const mock_socket = mock_sock.connect('testhost', 'testport')
264
+ mock_socket.writable = true
265
+
266
+ await new Promise((resolve, reject) => {
267
+ const orig = outbound_context.exports.send_email
268
+ outbound_context.exports.send_email = (from, to, contents, cb, opts) => {
269
+ try {
270
+ assert.match(contents, /^Content-type: message\/delivery-status/m)
271
+ assert.match(contents, /^Final-Recipient: rfc822;recipient@domain/m)
272
+ assert.match(contents, /^Action: failed/m)
273
+ assert.match(contents, /^Status: 5\.6\.0/m)
274
+ assert.match(contents, /I never did and will like ascii art cats/)
275
+ resolve()
276
+ } catch (e) {
277
+ reject(e)
278
+ } finally {
279
+ outbound_context.exports.send_email = orig
280
+ }
281
+ }
282
+ util_hmailitem.playTestSmtpConversation(
283
+ mock_hmail,
284
+ mock_socket,
285
+ reject,
286
+ [
287
+ { from: 'remote', line: '220 testing-smtp' },
288
+ { from: 'haraka', test: (l) => l.match(/^EHLO /), description: 'EHLO' },
289
+ { from: 'remote', line: '220-testing-smtp' },
290
+ { from: 'remote', line: '220 8BITMIME' },
291
+ { from: 'haraka', test: 'MAIL FROM:<sender@domain>' },
292
+ { from: 'remote', line: '250 2.1.0 Ok' },
293
+ { from: 'haraka', test: 'RCPT TO:<recipient@domain>' },
294
+ { from: 'remote', line: '250 2.1.5 Ok' },
295
+ { from: 'haraka', test: 'DATA' },
296
+ { from: 'remote', line: '550 5.6.0 I never did and will like ascii art cats.' },
297
+ { from: 'haraka', test: 'QUIT', end_test: true },
298
+ ],
299
+ () => {},
300
+ )
301
+ })
302
+ })
303
+ })