Haraka 3.1.7 → 3.2.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
4
4
 
5
5
  ### Unreleased
6
6
 
7
+ - fix(deps): update haraka-plugin-dkim to 1.2.0 to match new addresses handling #3564
8
+ - fix(deps): update haraka-plugin-rspamd to 1.6.0 to match new addresses handling #3564
9
+
10
+ ### [3.2.0] - 2026-05-NN
11
+
12
+ - fix(status): merge worker status into summary #3574
13
+ - dep: replace address-rfc282{1,2} with @haraka/email-address #3566
14
+ - change(BREAKING for some plugins), see https://github.com/haraka/Haraka/issues/3564
15
+
7
16
  ### [3.1.7] - 2026-05-19
8
17
 
9
18
  - feat(smtp_forward,smtp_proxy): honor `tls.ini` `[main]` and plugin `[tls]`
@@ -1862,3 +1871,4 @@ config files.
1862
1871
  [3.1.5]: https://github.com/haraka/Haraka/releases/tag/v3.1.5
1863
1872
  [3.1.6]: https://github.com/haraka/Haraka/releases/tag/v3.1.6
1864
1873
  [3.1.7]: https://github.com/haraka/Haraka/releases/tag/v3.1.7
1874
+ [3.2.0]: https://github.com/haraka/Haraka/releases/tag/v3.2.0
package/address.js ADDED
@@ -0,0 +1,53 @@
1
+ 'use strict'
2
+
3
+ // SUNSET 2027: Haraka core constructs envelope addresses with
4
+ // @haraka/email-address, whose `.address` / `.host` are string
5
+ // properties. Core itself — and a large body of bundled and
6
+ // third-party plugins — still use the historical address-rfc2821 /
7
+ // address-rfc2822 *method* contract (`addr.address()`, `addr.host()`).
8
+ //
9
+ // `asLegacy()` wraps each instance so both the new string-property API
10
+ // and the legacy callable form work during the transition. The wrap is
11
+ // idempotent, and `unwrapLegacy()` recovers the raw instance so the
12
+ // outbound queue's JSON re-hydration copies primitive string fields
13
+ // rather than the callable accessors.
14
+ //
15
+ // Once the ecosystem has migrated, delete this module, require
16
+ // `Address` straight from '@haraka/email-address', and drop the wrapper
17
+ // (see @haraka/email-address lib/legacy.js).
18
+
19
+ const { Address: BaseAddress, asLegacy, unwrapLegacy } = require('@haraka/email-address')
20
+
21
+ class Address extends BaseAddress {
22
+ constructor(...args) {
23
+ // never re-hydrate from a wrapped instance — copy raw strings
24
+ if (args.length) args[0] = unwrapLegacy(args[0])
25
+ // `new Address(user, host)` where `host` is another wrapped
26
+ // address's `.host` — the SUNSET-2027 callable accessor is
27
+ // `typeof 'function'`, which BaseAddress would mistake for an
28
+ // options object. Coerce it back to the primitive string.
29
+ if (args.length >= 2 && typeof args[1] === 'function') {
30
+ args[1] = String(args[1])
31
+ }
32
+ super(...args)
33
+ return asLegacy(this)
34
+ }
35
+
36
+ // Preserve the address-rfc2821 wire shape so existing on-disk queue
37
+ // files stay byte-compatible across the upgrade and re-hydrate
38
+ // unchanged. @haraka/email-address additionally carries
39
+ // phrase/comment/group/opts, which are irrelevant to envelope
40
+ // addresses and must not leak into the persisted todo. SUNSET 2027.
41
+ toJSON() {
42
+ const out = {
43
+ original: this.original,
44
+ original_host: this.original_host,
45
+ host: this.host,
46
+ user: this.user,
47
+ }
48
+ if (this.is_utf8) out.is_utf8 = true
49
+ return out
50
+ }
51
+ }
52
+
53
+ module.exports = { Address }
package/bin/haraka CHANGED
@@ -406,7 +406,7 @@ if (parsed.version) {
406
406
  plugins.load_plugins(parsed.test && parsed.test[0] !== 'all' ? parsed.test : null)
407
407
  const Connection = require(path.join(base, 'connection'))
408
408
  // var Transaction = require(path.join(base, "transaction"));
409
- const Address = require('address-rfc2821').Address
409
+ const Address = require('../address').Address
410
410
  const Notes = require('haraka-notes')
411
411
  const constants = require('haraka-constants')
412
412
  const client = {
package/connection.js CHANGED
@@ -12,7 +12,7 @@ const constants = require('haraka-constants')
12
12
  const net_utils = require('haraka-net-utils')
13
13
  const Notes = require('haraka-notes')
14
14
  const utils = require('haraka-utils')
15
- const { Address } = require('address-rfc2821')
15
+ const { Address } = require('./address')
16
16
  const ResultStore = require('haraka-results')
17
17
 
18
18
  // Haraka libs
@@ -1039,7 +1039,7 @@ class Connection {
1039
1039
  this.lognotice(dmsg, {
1040
1040
  code: constants.translate(retval === constants.cont ? constants.ok : retval),
1041
1041
  msg: msg || '',
1042
- sender: this.transaction.mail_from.address(),
1042
+ sender: this.transaction.mail_from.address,
1043
1043
  })
1044
1044
  switch (retval) {
1045
1045
  case constants.deny:
@@ -1089,7 +1089,7 @@ class Connection {
1089
1089
  this.lognotice(dmsg, {
1090
1090
  code: constants.translate(retval === constants.cont ? constants.ok : retval),
1091
1091
  msg: msg || '',
1092
- sender: this.transaction.mail_from.address(),
1092
+ sender: this.transaction.mail_from.address,
1093
1093
  })
1094
1094
  }
1095
1095
  switch (retval) {
package/docs/Outbound.md CHANGED
@@ -203,7 +203,7 @@ Options accepted by `send_email(from, to, contents, next, options)`:
203
203
 
204
204
  To send an already-built `Transaction` directly, use `outbound.send_trans_email(transaction, next)`. This is what `send_email()` calls internally and fires the `pre_send_trans_email` hook.
205
205
 
206
- <a name="fn1">1</a>: `Address` objects are [address-rfc2821](https://github.com/haraka/node-address-rfc2821) objects.
206
+ <a name="fn1">1</a>: `Address` objects are [@haraka/email-address](https://github.com/haraka/email-address) objects.
207
207
 
208
208
  [url-tls]: plugins/tls.md
209
209
  [url-harakamx]: https://github.com/haraka/haraka-net-utils?tab=readme-ov-file#harakamx
@@ -132,4 +132,4 @@ Append a banner to the end of the message. If `html` is omitted, each newline in
132
132
 
133
133
  Register a filter applied to body parts. `ct_match` is either a regex matched against the content-type line, or a string matched as a prefix (e.g. `/^text\/html/` or `'text/plain'`). `filter` receives `(content_type, encoding, buffer)` and must return a `Buffer` with the replacement body (in the same encoding).
134
134
 
135
- [address]: https://github.com/haraka/node-address-rfc2821
135
+ [address]: https://github.com/haraka/email-address
@@ -11,15 +11,31 @@ This plugin allows to get internal status of queues and pools with SMTP commands
11
11
 
12
12
  ```
13
13
  < 220 example.com ESMTP Haraka ready
14
- > STATUS QUEUE LIST
14
+ > STATUS QUEUE INSPECT
15
15
  < 211 {"delivery_queue":[],"temp_fail_queue":[]}
16
16
  ```
17
17
 
18
18
  ## Available commands list
19
19
 
20
- - `STATUS POOL LIST` - list of active pools
21
- - `STATUS QUEUE STATS` - queue statistics in format "<in_progress>/<delivery_queue length>/<temp_fail_queue length>"
22
- - `STATUS QUEUE LIST` - list of parsed queue files with _uuid, domain, mail_from, rcpt_to_ attributes
23
- - `STATUS QUEUE INSPECT` - returns content of _outbound.delivery_queue_ and _outbound.temp_fail_queue_
20
+ - `STATUS POOL LIST` - map of active outbound connection pools, keyed by `host:port`
21
+ - `STATUS QUEUE STATS` - queue statistics in format `"<in_progress>/<delivery_queue length>/<temp_fail_queue length>"`
22
+ - `STATUS QUEUE LIST` - list of queue files on disk with _uuid, domain, mail_from, rcpt_to_ attributes
23
+ - `STATUS QUEUE INSPECT` - returns merged content of `outbound.delivery_queue` and `outbound.temp_fail_queue` across all workers
24
24
  - `STATUS QUEUE DISCARD file` - stop delivering email file
25
25
  - `STATUS QUEUE PUSH file` - try to re-deliver email immediately
26
+
27
+ ## Notes
28
+
29
+ ### Live data only
30
+
31
+ `POOL LIST`, `QUEUE STATS`, and `QUEUE INSPECT` reflect live in-memory state. They show only messages currently being processed or waiting in the retry queue. `QUEUE LIST` reads queue files from disk and may show messages that have already been delivered if they haven't been cleaned up yet.
32
+
33
+ ### Cluster mode
34
+
35
+ In cluster mode, `POOL LIST`, `QUEUE STATS`, and `QUEUE INSPECT` aggregate results from all worker processes into a single response:
36
+
37
+ - `POOL LIST` — pool maps from all workers are merged into one object
38
+ - `QUEUE STATS` — counters from all workers are summed into a single `"N/N/N"` string
39
+ - `QUEUE INSPECT` — `delivery_queue` and `temp_fail_queue` arrays from all workers are concatenated
40
+
41
+ `QUEUE LIST` always runs on the master process since it reads shared queue files from disk.
package/outbound/hmail.js CHANGED
@@ -7,7 +7,7 @@ const dns = require('node:dns')
7
7
  const net = require('node:net')
8
8
  const path = require('node:path')
9
9
 
10
- const { Address } = require('address-rfc2821')
10
+ const { Address } = require('../address')
11
11
  const config = require('haraka-config')
12
12
  const constants = require('haraka-constants')
13
13
  const DSN = require('haraka-dsn')
@@ -1148,7 +1148,7 @@ class HMailItem extends events.EventEmitter {
1148
1148
  }
1149
1149
  for (const rcpt_to of this.todo.rcpt_to) {
1150
1150
  bounce_body.push(CRLF)
1151
- bounce_body.push(`Final-Recipient: rfc822;${rcpt_to.address()}${CRLF}`)
1151
+ bounce_body.push(`Final-Recipient: rfc822;${rcpt_to.address}${CRLF}`)
1152
1152
  let dsn_action = null
1153
1153
  if (rcpt_to.dsn_action) {
1154
1154
  dsn_action = rcpt_to.dsn_action
package/outbound/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const fs = require('node:fs/promises')
4
4
  const path = require('node:path')
5
5
 
6
- const { Address } = require('address-rfc2821')
6
+ const { Address } = require('../address')
7
7
  const config = require('haraka-config')
8
8
  const constants = require('haraka-constants')
9
9
  const net_utils = require('haraka-net-utils')
package/outbound/queue.js CHANGED
@@ -4,7 +4,7 @@ const child_process = require('node:child_process')
4
4
  const fs = require('node:fs/promises')
5
5
  const path = require('node:path')
6
6
 
7
- const { Address } = require('address-rfc2821')
7
+ const { Address } = require('../address')
8
8
  const config = require('haraka-config')
9
9
 
10
10
  const logger = require('../logger')
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "server",
10
10
  "email"
11
11
  ],
12
- "version": "3.1.7",
12
+ "version": "3.2.1",
13
13
  "homepage": "http://haraka.github.io",
14
14
  "repository": {
15
15
  "type": "git",
@@ -20,8 +20,7 @@
20
20
  "node": ">=20"
21
21
  },
22
22
  "dependencies": {
23
- "address-rfc2821": "~2.2.0",
24
- "address-rfc2822": "~2.2.3",
23
+ "@haraka/email-address": "~3.1.3",
25
24
  "haraka-config": "~1.6.0",
26
25
  "haraka-constants": "~1.0.7",
27
26
  "haraka-dsn": "~1.2.0",
@@ -47,7 +46,7 @@
47
46
  "haraka-plugin-bounce": "~2.1.2",
48
47
  "haraka-plugin-clamd": "~1.0.2",
49
48
  "haraka-plugin-dcc": "~1.0.3",
50
- "haraka-plugin-dkim": "~1.1.2",
49
+ "haraka-plugin-dkim": "~1.2.0",
51
50
  "haraka-plugin-dns-list": "~1.2.4",
52
51
  "haraka-plugin-early_talker": "~1.0.2",
53
52
  "haraka-plugin-fcrdns": "~1.1.2",
@@ -62,7 +61,7 @@
62
61
  "haraka-plugin-messagesniffer": "~1.0.1",
63
62
  "haraka-plugin-qmail-deliverable": "~1.3.5",
64
63
  "haraka-plugin-relay": "~1.0.2",
65
- "haraka-plugin-rspamd": "~1.5.0",
64
+ "haraka-plugin-rspamd": "~1.6.0",
66
65
  "haraka-plugin-spamassassin": "~1.0.4",
67
66
  "haraka-plugin-spf": "~1.2.11",
68
67
  "haraka-plugin-syslog": "~1.1.0",
@@ -25,12 +25,12 @@ exports.hook_data_post = function (next, connection) {
25
25
  }
26
26
 
27
27
  // Check recipient is the right one
28
- if (connection.transaction.rcpt_to[0].address().toLowerCase() != recip) {
28
+ if (connection.transaction.rcpt_to[0].address.toLowerCase() != recip) {
29
29
  return next()
30
30
  }
31
31
 
32
32
  // Check sender is in list
33
- const sender = connection.transaction.mail_from.address()
33
+ const sender = connection.transaction.mail_from.address
34
34
  if (!utils.in_array(sender, senders)) {
35
35
  return next(DENY, `You are not allowed to block mail, ${sender}`)
36
36
  }
@@ -69,7 +69,7 @@ exports.get_config = function (conn) {
69
69
  if (this.cfg.main.domain_selector === 'mail_from') {
70
70
  if (!conn.transaction.mail_from) return this.cfg.main
71
71
  dom = conn.transaction.mail_from.host
72
- address = conn.transaction.mail_from.address()
72
+ address = conn.transaction.mail_from.address
73
73
  } else {
74
74
  if (!conn.transaction.rcpt_to[0]) return this.cfg.main
75
75
  dom = conn.transaction.rcpt_to[0].host
@@ -92,7 +92,7 @@ exports.check_sender = function (next, connection, params) {
92
92
  const txn = connection?.transaction
93
93
  if (!txn) return
94
94
 
95
- const email = params[0].address()
95
+ const email = params[0].address
96
96
  if (!email) {
97
97
  txn.results.add(this, { skip: 'mail_from.null', emit: true })
98
98
  return next()
@@ -26,7 +26,7 @@ exports.hook_mail = function (next, connection, params) {
26
26
  const txn = connection?.transaction
27
27
  if (!txn) return
28
28
 
29
- const email = params[0].address()
29
+ const email = params[0].address
30
30
  if (!email) {
31
31
  txn.results.add(this, { skip: 'mail_from.null', emit: true })
32
32
  return next()
@@ -4,14 +4,14 @@
4
4
 
5
5
  exports.hook_rcpt = (next, connection, params) => {
6
6
  if (connection?.transaction) {
7
- connection.transaction.add_header('X-Envelope-To', params[0].address())
7
+ connection.transaction.add_header('X-Envelope-To', params[0].address)
8
8
  }
9
9
  next()
10
10
  }
11
11
 
12
12
  exports.hook_mail = (next, connection, params) => {
13
13
  if (connection?.transaction) {
14
- connection.transaction.add_header('X-Envelope-From', params[0].address())
14
+ connection.transaction.add_header('X-Envelope-From', params[0].address)
15
15
  }
16
16
  next()
17
17
  }
package/plugins/status.js CHANGED
@@ -171,7 +171,8 @@ exports.hook_init_master = function (next) {
171
171
  if (msg.event !== 'status.request') return
172
172
 
173
173
  plugin.call_workers(msg, (response) => {
174
- msg.result = response.filter((el) => el != null)
174
+ const valid = response.filter((el) => el != null)
175
+ msg.result = plugin.merge_worker_responses(msg.params, valid)
175
176
  msg.event = 'status.result'
176
177
  sender.send(msg)
177
178
  })
@@ -212,13 +213,41 @@ exports.call_master = (cmd, cb) => {
212
213
 
213
214
  exports.call_workers = function (cmd, cb) {
214
215
  Promise.allSettled(Object.values(server.cluster.workers).map((w) => this.call_worker(w, cmd))).then((r) => {
215
- cb(
216
- // r.filter(s => s.status === 'rejected').flatMap(s => s.reason),
217
- r.filter((s) => s.status === 'fulfilled').flatMap((s) => s.value),
218
- )
216
+ cb(r.filter((s) => s.status === 'fulfilled').map((s) => s.value))
219
217
  })
220
218
  }
221
219
 
220
+ // Merge per-worker responses into a single result matching non-cluster output shape.
221
+ exports.merge_worker_responses = (params, results) => {
222
+ const cmd = params.trim().split(/\s+/).slice(0, 2).join(' ').toUpperCase()
223
+
224
+ switch (cmd) {
225
+ case 'POOL LIST': {
226
+ return Object.assign({}, ...results)
227
+ }
228
+ case 'QUEUE INSPECT': {
229
+ const merged = { delivery_queue: [], temp_fail_queue: [] }
230
+ for (const r of results) {
231
+ if (!r) continue
232
+ if (Array.isArray(r.delivery_queue)) merged.delivery_queue.push(...r.delivery_queue)
233
+ if (Array.isArray(r.temp_fail_queue)) merged.temp_fail_queue.push(...r.temp_fail_queue)
234
+ }
235
+ return merged
236
+ }
237
+ case 'QUEUE STATS': {
238
+ const totals = [0, 0, 0]
239
+ for (const r of results) {
240
+ if (!r) continue
241
+ const parts = String(r).split('/')
242
+ for (let i = 0; i < 3; i++) totals[i] += parseInt(parts[i] ?? 0, 10)
243
+ }
244
+ return totals.join('/')
245
+ }
246
+ default:
247
+ return results
248
+ }
249
+ }
250
+
222
251
  // sends command to worker and then wait for response or timeout
223
252
  exports.call_worker = (worker, cmd) => {
224
253
  return new Promise((resolve) => {
@@ -5,7 +5,7 @@ const assert = require('node:assert/strict')
5
5
 
6
6
  const constants = require('haraka-constants')
7
7
  const DSN = require('haraka-dsn')
8
- const { Address } = require('address-rfc2821')
8
+ const { Address } = require('../address')
9
9
 
10
10
  const connection = require('../connection')
11
11
  const Server = require('../server')
@@ -2,7 +2,7 @@
2
2
 
3
3
  const assert = require('node:assert')
4
4
 
5
- const { Address } = require('address-rfc2821')
5
+ const { Address } = require('../../address')
6
6
  const fixtures = require('haraka-test-fixtures')
7
7
 
8
8
  /**
@@ -211,7 +211,7 @@ describe('outbound', () => {
211
211
  it('yields to setImmediate before opening process_delivery pipes', async () => {
212
212
  const stream = require('node:stream')
213
213
  const Transaction = require('../../transaction')
214
- const Address = require('address-rfc2821').Address
214
+ const Address = require('../../address').Address
215
215
  const outbound = require('../../outbound')
216
216
  const plugins = require('../../plugins')
217
217
 
@@ -271,7 +271,7 @@ describe('outbound', () => {
271
271
 
272
272
  it('adds missing Message-Id/Date and prepends Received before queueing', async () => {
273
273
  process.env.HARAKA_TEST_DIR = path.resolve('test')
274
- const Address = require('address-rfc2821').Address
274
+ const Address = require('../../address').Address
275
275
  const outbound = require('../../outbound')
276
276
  const plugins = require('../../plugins')
277
277
 
@@ -2,7 +2,7 @@
2
2
  const assert = require('node:assert')
3
3
  const { describe, it, beforeEach } = require('node:test')
4
4
 
5
- const { Address } = require('address-rfc2821')
5
+ const { Address } = require('../../../address')
6
6
  const fixtures = require('haraka-test-fixtures')
7
7
  const utils = require('haraka-utils')
8
8
 
@@ -6,7 +6,7 @@ const assert = require('node:assert/strict')
6
6
  const { describe, it, beforeEach, after } = require('node:test')
7
7
 
8
8
  const fixtures = require('haraka-test-fixtures')
9
- const { Address } = require('address-rfc2821')
9
+ const { Address } = require('@haraka/email-address')
10
10
  require('haraka-constants').import(global)
11
11
 
12
12
  // block_me appends to <config>/mail_from.blocklist when a sender is blocked;
@@ -5,7 +5,7 @@ const assert = require('node:assert/strict')
5
5
  const { EventEmitter } = require('node:events')
6
6
  const path = require('node:path')
7
7
 
8
- const { Address } = require('address-rfc2821')
8
+ const { Address } = require('../../../address')
9
9
  const fixtures = require('haraka-test-fixtures')
10
10
  const Notes = require('haraka-notes')
11
11
 
@@ -2,7 +2,7 @@
2
2
  const assert = require('node:assert/strict')
3
3
  const { describe, it, beforeEach } = require('node:test')
4
4
 
5
- const { Address } = require('address-rfc2821')
5
+ const { Address } = require('../../address')
6
6
  const fixtures = require('haraka-test-fixtures')
7
7
 
8
8
  const _set_up = () => {
@@ -2,7 +2,7 @@
2
2
  const assert = require('node:assert/strict')
3
3
  const { describe, it, beforeEach } = require('node:test')
4
4
 
5
- const { Address } = require('address-rfc2821')
5
+ const { Address } = require('../../address')
6
6
  const fixtures = require('haraka-test-fixtures')
7
7
  require('haraka-constants').import(global)
8
8
 
@@ -1,9 +1,9 @@
1
1
  'use strict'
2
2
 
3
- const assert = require('node:assert/strict')
3
+ const assert = require('node:assert')
4
4
  const { describe, it, beforeEach } = require('node:test')
5
5
 
6
- const { Address } = require('address-rfc2821')
6
+ const { Address } = require('../../address')
7
7
  const fixtures = require('haraka-test-fixtures')
8
8
 
9
9
  const _set_up = () => {
@@ -134,4 +134,75 @@ describe('status', () => {
134
134
  this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'QUEUE PUSH file'])
135
135
  })
136
136
  })
137
+
138
+ describe('merge_worker_responses', () => {
139
+ beforeEach(_set_up)
140
+
141
+ it('POOL LIST merges objects from all workers', () => {
142
+ const result = JSON.parse(
143
+ JSON.stringify(
144
+ this.plugin.merge_worker_responses('POOL LIST', [
145
+ { 'host1:25': { inUse: 1, size: 3 } },
146
+ { 'host2:25': { inUse: 0, size: 2 } },
147
+ {},
148
+ ]),
149
+ ),
150
+ )
151
+ assert.deepEqual(result, {
152
+ 'host1:25': { inUse: 1, size: 3 },
153
+ 'host2:25': { inUse: 0, size: 2 },
154
+ })
155
+ })
156
+
157
+ it('POOL LIST with all empty workers returns empty object', () => {
158
+ const result = JSON.parse(JSON.stringify(this.plugin.merge_worker_responses('POOL LIST', [{}, {}, {}])))
159
+ assert.deepEqual(result, {})
160
+ })
161
+
162
+ it('QUEUE INSPECT merges queues from all workers', () => {
163
+ const result = JSON.parse(
164
+ JSON.stringify(
165
+ this.plugin.merge_worker_responses('QUEUE INSPECT', [
166
+ { delivery_queue: [{ id: 'a' }], temp_fail_queue: [{ id: 'x', fire_time: 1 }] },
167
+ { delivery_queue: [{ id: 'b' }], temp_fail_queue: [] },
168
+ { delivery_queue: [], temp_fail_queue: [{ id: 'y', fire_time: 2 }] },
169
+ ]),
170
+ ),
171
+ )
172
+ assert.deepEqual(result, {
173
+ delivery_queue: [{ id: 'a' }, { id: 'b' }],
174
+ temp_fail_queue: [
175
+ { id: 'x', fire_time: 1 },
176
+ { id: 'y', fire_time: 2 },
177
+ ],
178
+ })
179
+ })
180
+
181
+ it('QUEUE INSPECT with all empty queues returns empty lists', () => {
182
+ const result = JSON.parse(
183
+ JSON.stringify(
184
+ this.plugin.merge_worker_responses('QUEUE INSPECT', [
185
+ { delivery_queue: [], temp_fail_queue: [] },
186
+ { delivery_queue: [], temp_fail_queue: [] },
187
+ ]),
188
+ ),
189
+ )
190
+ assert.deepEqual(result, { delivery_queue: [], temp_fail_queue: [] })
191
+ })
192
+
193
+ it('QUEUE STATS sums across workers', () => {
194
+ const result = this.plugin.merge_worker_responses('QUEUE STATS', ['1/2/3', '0/1/0', '2/0/1'])
195
+ assert.equal(result, '3/3/4')
196
+ })
197
+
198
+ it('QUEUE STATS with all zeros', () => {
199
+ const result = this.plugin.merge_worker_responses('QUEUE STATS', ['0/0/0', '0/0/0', '0/0/0'])
200
+ assert.equal(result, '0/0/0')
201
+ })
202
+
203
+ it('unknown command returns results array unchanged', () => {
204
+ const result = this.plugin.merge_worker_responses('POOL UNKNOWN', [{ foo: 1 }, { foo: 2 }])
205
+ assert.equal(result.length, 2)
206
+ })
207
+ })
137
208
  })
@@ -5,7 +5,7 @@ const assert = require('node:assert/strict')
5
5
  const { PassThrough } = require('node:stream')
6
6
  const path = require('node:path')
7
7
 
8
- const { Address } = require('address-rfc2821')
8
+ const { Address } = require('../address')
9
9
  const fixtures = require('haraka-test-fixtures')
10
10
  const net_utils = require('haraka-net-utils')
11
11
  const message = require('haraka-email-message')