Haraka 3.1.2 → 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 (66) hide show
  1. package/.prettierignore +2 -0
  2. package/CONTRIBUTORS.md +24 -2
  3. package/Changes.md +48 -0
  4. package/Plugins.md +81 -64
  5. package/README.md +1 -1
  6. package/bin/haraka +9 -7
  7. package/config/connection.ini +10 -0
  8. package/config/smtp.ini +0 -9
  9. package/connection.js +15 -19
  10. package/docs/CoreConfig.md +2 -3
  11. package/docs/Plugins.md +1 -1
  12. package/docs/plugins/aliases.md +0 -2
  13. package/docs/plugins/queue/qmail-queue.md +0 -1
  14. package/docs/tutorials/Migrating_from_v1_to_v2.md +1 -1
  15. package/logger.js +2 -2
  16. package/outbound/client_pool.js +1 -1
  17. package/outbound/hmail.js +76 -83
  18. package/outbound/index.js +36 -34
  19. package/outbound/queue.js +231 -176
  20. package/package.json +29 -31
  21. package/plugins/prevent_credential_leaks.js +2 -2
  22. package/plugins/process_title.js +1 -1
  23. package/plugins/queue/smtp_forward.js +1 -1
  24. package/plugins/status.js +8 -5
  25. package/plugins/tls.js +1 -1
  26. package/plugins.js +19 -14
  27. package/rfc1869.js +10 -10
  28. package/run_tests +20 -2
  29. package/server.js +15 -10
  30. package/smtp_client.js +2 -9
  31. package/test/config/tls/haraka.local.pem +47 -47
  32. package/test/connection.js +286 -147
  33. package/test/fixtures/line_socket.js +1 -0
  34. package/test/fixtures/util_hmailitem.js +1 -1
  35. package/test/outbound/bounce_net_errors.js +176 -0
  36. package/test/outbound/bounce_rfc3464.js +303 -0
  37. package/test/outbound/hmail.js +140 -104
  38. package/test/outbound/index.js +61 -101
  39. package/test/outbound/qfile.js +25 -25
  40. package/test/outbound/queue.js +233 -0
  41. package/test/plugins/queue/smtp_forward.js +1 -1
  42. package/test/plugins/record_envelope_addresses.js +93 -0
  43. package/test/plugins/tls.js +2 -2
  44. package/test/plugins/xclient.js +137 -0
  45. package/test/rfc1869.js +43 -0
  46. package/test/smtp_client.js +6 -6
  47. package/test/transaction.js +486 -201
  48. package/tls_socket.js +3 -3
  49. package/transaction.js +33 -10
  50. package/config/me +0 -1
  51. package/config/rabbitmq.ini +0 -10
  52. package/config/rabbitmq_amqplib.ini +0 -19
  53. package/config/tls_cert.pem +0 -23
  54. package/config/tls_key.pem +0 -28
  55. package/docs/plugins/queue/rabbitmq.md +0 -34
  56. package/docs/plugins/queue/rabbitmq_amqplib.md +0 -51
  57. package/plugins/queue/rabbitmq.js +0 -141
  58. package/plugins/queue/rabbitmq_amqplib.js +0 -96
  59. package/test/config/tls/ec.pem +0 -23
  60. package/test/config/tls/mismatched.pem +0 -49
  61. package/test/outbound_bounce_net_errors.js +0 -157
  62. package/test/outbound_bounce_rfc3464.js +0 -366
  63. package/test/queue/multibyte +0 -0
  64. package/test/queue/plain +0 -0
  65. package/test/test-queue/delete-me +0 -0
  66. package/test/tls_socket.js +0 -273
package/connection.js CHANGED
@@ -191,11 +191,7 @@ class Connection {
191
191
  })
192
192
 
193
193
  const ha_list = net.isIPv6(self.remote.ip) ? haproxy_hosts_ipv6 : haproxy_hosts_ipv4
194
- if (
195
- ha_list.some((element, index, array) => {
196
- return ipaddr.parse(self.remote.ip).match(element[0], element[1])
197
- })
198
- ) {
194
+ if (ha_list.some((element) => ipaddr.parse(self.remote.ip).match(element[0], element[1]))) {
199
195
  self.proxy.allowed = true
200
196
  // Wait for PROXY command
201
197
  self.proxy.timer = setTimeout(() => {
@@ -302,7 +298,7 @@ class Connection {
302
298
  /* eslint no-control-regex: 0 */
303
299
  if (/[^\x00-\x7F]/.test(this.current_line)) {
304
300
  // See if this is a TLS handshake
305
- const buf = Buffer.from(this.current_line.substr(0, 3), 'binary')
301
+ const buf = Buffer.from(this.current_line.slice(0, 3), 'binary')
306
302
  if (
307
303
  buf[0] === 0x16 &&
308
304
  buf[1] === 0x03 &&
@@ -348,10 +344,13 @@ class Connection {
348
344
  } else if (this.state === states.LOOP) {
349
345
  // Allow QUIT
350
346
  if (this.current_line.toUpperCase() === 'QUIT') {
347
+ this.state = states.PAUSE_SMTP
351
348
  this.cmd_quit()
352
349
  } else {
353
350
  this.respond(this.loop_code, this.loop_msg)
354
351
  }
352
+ } else if (this.state === states.PAUSE_SMTP) {
353
+ // Do nothing
355
354
  } else {
356
355
  throw new Error(`unknown state ${this.state}`)
357
356
  }
@@ -516,9 +515,7 @@ class Connection {
516
515
  } else {
517
516
  messages = msg.slice()
518
517
  }
519
- messages = messages.filter((msg2) => {
520
- return /\S/.test(msg2)
521
- })
518
+ messages = messages.filter((msg2) => /\S/.test(msg2))
522
519
 
523
520
  // Multiline AUTH PLAIN as in RFC-4954 page 8.
524
521
  if (code === 334 && !messages.length) {
@@ -530,7 +527,7 @@ class Connection {
530
527
  if (cfg.uuid.deny_chars) {
531
528
  uuid = (this.transaction || this).uuid
532
529
  if (cfg.uuid.deny_chars > 1) {
533
- uuid = uuid.substr(0, cfg.uuid.deny_chars)
530
+ uuid = uuid.slice(0, cfg.uuid.deny_chars)
534
531
  }
535
532
  }
536
533
  }
@@ -673,10 +670,10 @@ class Connection {
673
670
  resume() {
674
671
  if (this.state >= states.DISCONNECTING) return
675
672
  this.client.resume()
676
- if (this.prev_state) {
673
+ if (this.prev_state && this.state === states.PAUSE_DATA) {
677
674
  this.state = this.prev_state
678
- this.prev_state = null
679
675
  }
676
+ this.prev_state = null
680
677
  setImmediate(() => this._process_data())
681
678
  }
682
679
  /////////////////////////////////////////////////////////////////////////////
@@ -793,12 +790,12 @@ class Connection {
793
790
  greeting = [...cfg.message.greeting]
794
791
  greeting[0] = `${this.local.host} ESMTP ${greeting[0]}`
795
792
  if (cfg.uuid.banner_chars) {
796
- greeting[0] += ` (${this.uuid.substr(0, cfg.uuid.banner_chars)})`
793
+ greeting[0] += ` (${this.uuid.slice(0, cfg.uuid.banner_chars)})`
797
794
  }
798
795
  } else {
799
796
  greeting = `${this.local.host} ESMTP ${this.local.info} ready`
800
797
  if (cfg.uuid.banner_chars) {
801
- greeting += ` (${this.uuid.substr(0, cfg.uuid.banner_chars)})`
798
+ greeting += ` (${this.uuid.slice(0, cfg.uuid.banner_chars)})`
802
799
  }
803
800
  }
804
801
  this.respond(220, msg || greeting)
@@ -964,7 +961,7 @@ class Connection {
964
961
  let addr = sender.format()
965
962
  if (addr.length > 2) {
966
963
  // all but null sender
967
- addr = addr.substr(1, addr.length - 2) // trim off < >
964
+ addr = addr.slice(1, -1) // trim off < >
968
965
  }
969
966
  this.transaction.results.add(
970
967
  { name: 'mail_from' },
@@ -1012,7 +1009,7 @@ class Connection {
1012
1009
 
1013
1010
  const addr = rcpt.format()
1014
1011
  const recipient = {
1015
- address: addr.substr(1, addr.length - 2),
1012
+ address: addr.slice(1, -1),
1016
1013
  action,
1017
1014
  }
1018
1015
 
@@ -1446,10 +1443,9 @@ class Connection {
1446
1443
  }
1447
1444
 
1448
1445
  // assemble the new header
1449
- let header = [this.local.host]
1450
- header = header.concat(this.notes.authentication_results)
1446
+ let header = [this.local.host, ...this.notes.authentication_results]
1451
1447
  if (has_tran === true) {
1452
- header = header.concat(this.transaction.notes.authentication_results)
1448
+ header = [...header, ...this.transaction.notes.authentication_results]
1453
1449
  }
1454
1450
  if (header.length === 1) return '' // no results
1455
1451
  return header.join(';\r\n\t')
@@ -30,9 +30,6 @@ The list of plugins to load
30
30
  - daemonize - enable this to cause Haraka to fork into the background on start-up (default: 0)
31
31
  - daemon_log_file - (default: /var/log/haraka.log) where to redirect stdout/stderr when daemonized
32
32
  - daemon_pid_file - (default: /var/run/haraka.pid) where to write a PID file to
33
- - spool_dir - (default: none) directory to create temporary spool files in
34
- - spool_after - (default: -1) if message exceeds this size in bytes, then spool the message to disk
35
- specify -1 to disable spooling completely or 0 to force all messages to be spooled to disk.
36
33
  - graceful_shutdown - (default: false) enable this to wait for sockets on shutdown instead of closing them quickly
37
34
  - force_shutdown_timeout - (default: 30) number of seconds to wait for a graceful shutdown
38
35
 
@@ -45,6 +42,8 @@ The list of plugins to load
45
42
 
46
43
  See inline comments in connection.ini for the following settings:
47
44
 
45
+ - main.spool_dir
46
+ - main.spool_after
48
47
  - haproxy.hosts_ipv4
49
48
  - haproxy.hosts_ipv6
50
49
  - headers.\*
package/docs/Plugins.md CHANGED
@@ -166,7 +166,7 @@ These are the hook and their parameters (next excluded):
166
166
  - rcpt_ok (to)
167
167
  - data - called at the DATA command
168
168
  - data_post - called at the end-of-data marker
169
- - max_data_exceeded - called when the message exceeds connection.max_bytes
169
+ - max_data_exceeded - called when the message exceeds connection.max.bytes
170
170
  - queue - called to queue the mail
171
171
  - queue_outbound - called to queue the mail when connection.relaying is set
172
172
  - queue_ok - called when a mail has been queued successfully
@@ -97,7 +97,6 @@ WARNING: DO NOT USE THIS PLUGIN WITH queue/smtp_proxy.
97
97
  - action (required)
98
98
 
99
99
  The following is a list of supported actions, and the options they require.
100
-
101
100
  - drop
102
101
 
103
102
  This action simply drops a message, while pretending everything was
@@ -110,7 +109,6 @@ WARNING: DO NOT USE THIS PLUGIN WITH queue/smtp_proxy.
110
109
  "to" option. A note about matching in addition to the note
111
110
  about wildcard '-' above. When we match an alias, we store the
112
111
  hostname of the match for a shortcut substitution syntax later.
113
-
114
112
  - to (required)
115
113
 
116
114
  This option is the full address, or local part at matched hostname
@@ -10,7 +10,6 @@ for both inbound and outbound delivery.
10
10
  The path to the `qmail-queue` binary. Default: `/var/qmail/bin/qmail-queue`
11
11
 
12
12
  - qmail-queue.ini
13
-
14
13
  - enable_outbound=true
15
14
 
16
15
  Deliver outbound email to qmail. Set to false to use Haraka's
@@ -6,7 +6,7 @@ Streams are an abstraction over a data flow that is provided by Node core and is
6
6
 
7
7
  For more information about the Stream API, see http://nodejs.org/api/stream.html
8
8
 
9
- Note that when using bundled Haraka plugins, it's very unlikely you will need to change anything. Though you may want to configure `spool_dir` and `spool_after` in `config/smtp.ini`. If you have custom plugins, continue reading.
9
+ Note that when using bundled Haraka plugins, it's very unlikely you will need to change anything. Though you may want to configure `spool_dir` and `spool_after` in `config/smtp.ini` (v2.x), or in `config/connection.ini` (v3.1 or newer). If you have custom plugins, continue reading.
10
10
 
11
11
  ## Changes To Look For
12
12
 
package/logger.js CHANGED
@@ -255,9 +255,9 @@ logger.log_if_level = (level, key, origin) =>
255
255
  if (Object.hasOwn(data, 'uuid')) logobj.uuid = data.uuid
256
256
  if (data.todo?.uuid) logobj.uuid = data.todo.uuid // outbound/hmail
257
257
  } else if (logger.format === logger.formats.LOGFMT && data.constructor === Object) {
258
- logobj = Object.assign(logobj, data)
258
+ logobj = { ...logobj, ...data }
259
259
  } else if (logger.format === logger.formats.JSON && data.constructor === Object) {
260
- logobj = Object.assign(logobj, data)
260
+ logobj = { ...logobj, ...data }
261
261
  } else if (Object.hasOwn(data, 'uuid')) {
262
262
  // outbound/client_pool
263
263
  logobj.uuid = data.uuid
@@ -37,7 +37,7 @@ exports.get_client = function (mx, callback) {
37
37
  const errMsg = err.message
38
38
  ? err.message
39
39
  : err instanceof AggregateError
40
- ? err.map((e) => e.message).join(', ')
40
+ ? err.errors.map((e) => e.message).join(', ')
41
41
  : util.inspect(err, { depth: 3 })
42
42
  callback(errMsg, null)
43
43
  })
package/outbound/hmail.js CHANGED
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const events = require('node:events')
4
- const fs = require('node:fs')
4
+ const fs = require('node:fs/promises')
5
+ const { createReadStream } = require('node:fs')
5
6
  const dns = require('node:dns')
6
7
  const net = require('node:net')
7
8
  const path = require('node:path')
@@ -68,20 +69,15 @@ class HMailItem extends events.EventEmitter {
68
69
  }
69
70
 
70
71
  data_stream() {
71
- return fs.createReadStream(this.path, {
72
+ return createReadStream(this.path, {
72
73
  start: this.data_start,
73
74
  end: this.file_size,
74
75
  })
75
76
  }
76
77
 
77
- size_file() {
78
- fs.stat(this.path, (err, stats) => {
79
- if (err) {
80
- // we are fucked... guess I need somewhere for this to go
81
- this.logerror(`Error obtaining file size: ${err}`)
82
- this.temp_fail('Error obtaining file size')
83
- return
84
- }
78
+ async size_file() {
79
+ try {
80
+ const stats = await fs.stat(this.path)
85
81
  if (stats.size === 0) {
86
82
  this.logerror(`Error reading queue file ${this.filename}: zero bytes`)
87
83
  this.emit('error', `Error reading queue file ${this.filename}: zero bytes`)
@@ -90,74 +86,73 @@ class HMailItem extends events.EventEmitter {
90
86
 
91
87
  this.file_size = stats.size
92
88
  this.read_todo()
93
- })
89
+ } catch (err) {
90
+ // we are fucked... guess I need somewhere for this to go
91
+ this.logerror(`Error obtaining file size: ${err}`)
92
+ this.temp_fail('Error obtaining file size')
93
+ }
94
94
  }
95
95
 
96
- read_todo() {
97
- this._stream_bytes_from(this.path, { start: 0, end: 3 }, (err, bytes) => {
98
- if (err) {
99
- const errMsg = `Error reading queue file ${this.filename}: ${err}`
100
- this.logerror(errMsg)
101
- this.temp_fail(errMsg)
102
- return
103
- }
96
+ async read_todo() {
97
+ try {
98
+ const bytes = await this._stream_bytes_from(this.path, { start: 0, end: 3 })
104
99
 
105
100
  const todo_len = bytes.readUInt32BE(0)
106
101
  this.logdebug(`todo header length: ${todo_len}`)
107
102
  this.data_start = todo_len + 4
108
103
 
109
- this._stream_bytes_from(this.path, { start: 4, end: todo_len + 3 }, (err2, todo_bytes) => {
110
- if (todo_bytes.length !== todo_len) {
111
- const wrongLength = `Didn't find right amount of data in todo!: ${err2} ${this.path}`
112
- this.logcrit(wrongLength)
113
- fs.rename(this.path, path.join(queue_dir, `error.${this.filename}`), (err3) => {
114
- if (err3) {
115
- this.logerror(`Error creating (error.${this.filename}): ${err3}`)
116
- }
117
- })
118
- this.emit('error', wrongLength) // Note nothing picks this up yet
119
- return
120
- }
121
-
122
- // we read everything
123
- const todo_json = todo_bytes.toString().trim()
124
- const last_char = todo_json.charAt(todo_json.length - 1)
125
- if (last_char !== '}') {
126
- this.emit(
127
- 'error',
128
- `invalid todo header end char: ${last_char} at pos ${todo_len} of ${this.filename}`,
129
- )
130
- return
104
+ const todo_bytes = await this._stream_bytes_from(this.path, { start: 4, end: todo_len + 3 })
105
+ if (todo_bytes.length !== todo_len) {
106
+ const wrongLength = `Didn't find right amount of data in todo: ${this.path}`
107
+ this.logcrit(wrongLength)
108
+ try {
109
+ await fs.rename(this.path, path.join(queue_dir, `error.${this.filename}`))
110
+ } catch (renameErr) {
111
+ this.logerror(`Failed to move corrupt todo file ${this.path} to error queue: ${renameErr}`)
131
112
  }
132
- this.todo = JSON.parse(todo_json)
133
- this.todo.mail_from = new Address(this.todo.mail_from)
134
- this.todo.rcpt_to = this.todo.rcpt_to.map((a) => new Address(a))
135
- this.todo.notes = new Notes(this.todo.notes)
136
- this.emit('ready')
137
- })
138
- })
139
- }
113
+ this.emit('error', wrongLength) // Note nothing picks this up yet
114
+ return
115
+ }
140
116
 
141
- _stream_bytes_from(file_path, opts, done) {
142
- if (opts.encoding !== undefined) {
143
- // passing an encoding to fs.createReadStream will change the type of data returned
144
- // ex: instead of returning a buffer, it may return a String, which will cause
145
- // Buffer.concat to barf. There's a reason this function has 'bytes' in the name
146
- done(new Error('Thar be dragons here! Encode/decode on the result of this function'))
147
- return
117
+ // we read everything
118
+ const todo_json = todo_bytes.toString().trim()
119
+ const last_char = todo_json.charAt(todo_json.length - 1)
120
+ if (last_char !== '}') {
121
+ this.emit('error', `invalid todo header end char: ${last_char} at pos ${todo_len} of ${this.filename}`)
122
+ return
123
+ }
124
+ this.todo = JSON.parse(todo_json)
125
+ this.todo.mail_from = new Address(this.todo.mail_from)
126
+ this.todo.rcpt_to = this.todo.rcpt_to.map((a) => new Address(a))
127
+ this.todo.notes = new Notes(this.todo.notes)
128
+ this.emit('ready')
129
+ } catch (err) {
130
+ const errMsg = `Error reading queue file ${this.filename}: ${err}`
131
+ this.logerror(errMsg)
132
+ this.temp_fail(errMsg)
148
133
  }
134
+ }
149
135
 
150
- const stream = fs.createReadStream(file_path, opts)
151
-
152
- stream.on('error', done)
136
+ _stream_bytes_from(file_path, opts) {
137
+ return new Promise((resolve, reject) => {
138
+ if (opts.encoding !== undefined) {
139
+ // passing an encoding to fs.createReadStream will change the type of data returned
140
+ // ex: instead of returning a buffer, it may return a String, which will cause
141
+ // Buffer.concat to barf. There's a reason this function has 'bytes' in the name
142
+ reject(new Error('Thar be dragons here! Encode/decode on the result of this function'))
143
+ return
144
+ }
145
+ const stream = createReadStream(file_path, opts)
146
+ stream.on('error', reject)
153
147
 
154
- let raw_bytes = Buffer.alloc(0)
155
- stream.on('data', (data) => {
156
- raw_bytes = Buffer.concat([raw_bytes, data])
157
- })
148
+ let raw_bytes = Buffer.alloc(0)
149
+ stream.on('data', (data) => {
150
+ raw_bytes = Buffer.concat([raw_bytes, data])
151
+ })
158
152
 
159
- stream.on('end', () => {
160
- done(null, raw_bytes)
153
+ stream.on('end', () => {
154
+ resolve(raw_bytes)
155
+ })
161
156
  })
162
157
  }
163
158
 
@@ -365,7 +360,7 @@ class HMailItem extends events.EventEmitter {
365
360
  socket.emit('error', `socket timeout waiting on ${command}`)
366
361
  })
367
362
 
368
- socket.once('error', (err) => {
363
+ socket.on('error', (err) => {
369
364
  if (!processing_mail) return
370
365
 
371
366
  self.logerror(`Ongoing connection failed to ${host}:${port} : ${err}`)
@@ -1290,7 +1285,7 @@ class HMailItem extends events.EventEmitter {
1290
1285
 
1291
1286
  double_bounce(err) {
1292
1287
  this.lognotice(`Double bounce: ${err}`)
1293
- fs.unlink(this.path, () => {})
1288
+ fs.unlink(this.path).catch(() => {})
1294
1289
  this.next_cb()
1295
1290
  // TODO: fill this in... ?
1296
1291
  // One strategy is perhaps log to an mbox file. What do other servers do?
@@ -1320,7 +1315,7 @@ class HMailItem extends events.EventEmitter {
1320
1315
  this.refcount--
1321
1316
  if (this.refcount === 0) {
1322
1317
  // Remove the file.
1323
- fs.unlink(this.path, () => {})
1318
+ fs.unlink(this.path).catch(() => {})
1324
1319
  this.next_cb()
1325
1320
  }
1326
1321
  }
@@ -1349,7 +1344,7 @@ class HMailItem extends events.EventEmitter {
1349
1344
  plugins.run_hooks('deferred', this, { delay, err, ...(extra || {}) })
1350
1345
  }
1351
1346
 
1352
- deferred_respond(retval, msg, params) {
1347
+ async deferred_respond(retval, msg, params) {
1353
1348
  if (retval !== constants.cont && retval !== constants.denysoft) {
1354
1349
  this.loginfo(`plugin responded with: ${retval}. Not deferring. Deleting mail.`)
1355
1350
  return this.discard() // calls next_cb
@@ -1367,11 +1362,8 @@ class HMailItem extends events.EventEmitter {
1367
1362
  parts.attempts = this.num_failures
1368
1363
  const new_filename = _qfile.name(parts)
1369
1364
 
1370
- fs.rename(this.path, path.join(queue_dir, new_filename), (err) => {
1371
- if (err) {
1372
- return this.bounce(`Error re-queueing email: ${err}`)
1373
- }
1374
-
1365
+ try {
1366
+ await fs.rename(this.path, path.join(queue_dir, new_filename))
1375
1367
  this.path = path.join(queue_dir, new_filename)
1376
1368
  this.filename = new_filename
1377
1369
 
@@ -1380,7 +1372,9 @@ class HMailItem extends events.EventEmitter {
1380
1372
  temp_fail_queue.add(this.filename, delay, () => {
1381
1373
  delivery_queue.push(this)
1382
1374
  })
1383
- })
1375
+ } catch (err) {
1376
+ return this.bounce(`Error re-queueing email: ${err}`)
1377
+ }
1384
1378
  }
1385
1379
 
1386
1380
  // The following handler impacts outgoing mail. It removes the queue file.
@@ -1472,18 +1466,17 @@ class HMailItem extends events.EventEmitter {
1472
1466
  err_handler(err, 'hmail.data_stream reader')
1473
1467
  })
1474
1468
  rs.on('end', () => {
1475
- ws.on('close', () => {
1476
- const dest_path = path.join(queue_dir, fname)
1477
- fs.rename(tmp_path, dest_path, (err) => {
1478
- if (err) {
1479
- err_handler(err, 'tmp file rename')
1480
- return
1481
- }
1469
+ ws.on('close', async () => {
1470
+ try {
1471
+ const dest_path = path.join(queue_dir, fname)
1472
+ await fs.rename(tmp_path, dest_path)
1482
1473
  const split_mail = new HMailItem(fname, dest_path, hmail.notes)
1483
1474
  split_mail.once('ready', () => {
1484
1475
  cb(split_mail)
1485
1476
  })
1486
- })
1477
+ } catch (err) {
1478
+ err_handler(err, 'tmp file rename')
1479
+ }
1487
1480
  })
1488
1481
  ws.destroySoon()
1489
1482
  })
package/outbound/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const fs = require('node:fs')
3
+ const fs = require('node:fs/promises')
4
4
  const path = require('node:path')
5
5
 
6
6
  const { Address } = require('address-rfc2821')
@@ -42,30 +42,35 @@ const qlfns = [
42
42
  'flush_queue',
43
43
  'load_pid_queue',
44
44
  'ensure_queue_dir',
45
- 'load_queue',
45
+ 'init_queue',
46
46
  'stats',
47
47
  ]
48
48
  for (const n of qlfns) {
49
49
  exports[n] = queuelib[n]
50
50
  }
51
51
 
52
- process.on('message', (msg) => {
52
+ process.on('message', async (msg) => {
53
53
  if (!msg.event) return
54
54
 
55
- if (msg.event === 'outbound.load_pid_queue') {
56
- exports.load_pid_queue(msg.data)
57
- return
58
- }
59
- if (msg.event === 'outbound.flush_queue') {
60
- exports.flush_queue(msg.domain, process.pid)
61
- return
62
- }
63
- if (msg.event === 'outbound.shutdown') {
64
- logger.info(exports, 'Shutting down temp fail queue')
65
- temp_fail_queue.shutdown()
55
+ try {
56
+ if (msg.event === 'outbound.load_pid_queue') {
57
+ await exports.load_pid_queue(msg.data)
58
+ return
59
+ }
60
+ if (msg.event === 'outbound.flush_queue') {
61
+ await exports.flush_queue(msg.domain, process.pid)
62
+ return
63
+ }
64
+ if (msg.event === 'outbound.shutdown') {
65
+ logger.info(exports, 'Shutting down temp fail queue')
66
+ temp_fail_queue.shutdown()
67
+ return
68
+ }
69
+ // ignores the message
70
+ } catch (err) {
71
+ logger.error(exports, err)
66
72
  return
67
73
  }
68
- // ignores the message
69
74
  })
70
75
 
71
76
  exports.send_email = function (from, to, contents, next, options = {}) {
@@ -120,11 +125,11 @@ exports.send_email = function (from, to, contents, next, options = {}) {
120
125
  while ((match = utils.line_regexp.exec(contents))) {
121
126
  let line = match[1]
122
127
  line = line.replace(/\r?\n?$/, '\r\n') // make sure it ends in \r\n
123
- if (dot_stuffed === false && line.length >= 3 && line.substr(0, 1) === '.') {
128
+ if (dot_stuffed === false && line.length >= 3 && line.substring(0, 1) === '.') {
124
129
  line = `.${line}`
125
130
  }
126
131
  transaction.add_data(Buffer.from(line))
127
- contents = contents.substr(match[1].length)
132
+ contents = contents.substring(match[1].length)
128
133
  if (contents.length === 0) {
129
134
  break
130
135
  }
@@ -252,7 +257,7 @@ exports.send_trans_email = function (transaction, next) {
252
257
  }
253
258
  } catch (err) {
254
259
  for (let i = 0, l = ok_paths.length; i < l; i++) {
255
- fs.unlink(ok_paths[i], () => {})
260
+ await fs.unlink(ok_paths[i]).catch(() => {})
256
261
  }
257
262
  transaction.results.add({ name: 'outbound' }, { err })
258
263
  if (next) next(constants.denysoft, err)
@@ -279,32 +284,29 @@ exports.process_delivery = function (ok_paths, todo, hmails) {
279
284
  flags: constants.WRITE_EXCL,
280
285
  })
281
286
 
282
- ws.on('close', () => {
287
+ ws.on('close', async () => {
283
288
  const dest_path = path.join(queue_dir, fname)
284
- fs.rename(tmp_path, dest_path, (err) => {
285
- if (err) {
286
- logger.error(exports, `Unable to rename tmp file!: ${err}`)
287
- fs.unlink(tmp_path, () => {})
288
- reject('Queue error')
289
- } else {
290
- hmails.push(new HMailItem(fname, dest_path, todo.notes))
291
- ok_paths.push(dest_path)
292
- resolve()
293
- }
294
- })
289
+ try {
290
+ await fs.rename(tmp_path, dest_path)
291
+ hmails.push(new HMailItem(fname, dest_path, todo.notes))
292
+ ok_paths.push(dest_path)
293
+ resolve()
294
+ } catch (err) {
295
+ logger.error(exports, `Unable to rename tmp file: ${err}`)
296
+ await fs.unlink(tmp_path).catch(() => {})
297
+ reject('Queue error')
298
+ }
295
299
  })
296
300
 
297
- ws.on('error', (err) => {
301
+ ws.on('error', async (err) => {
298
302
  logger.error(exports, `Unable to write queue file (${fname}): ${err}`)
299
303
  ws.destroy()
300
- fs.unlink(tmp_path, () => {})
304
+ await fs.unlink(tmp_path).catch(() => {})
301
305
  reject('Queueing failed')
302
306
  })
303
307
 
304
308
  this.build_todo(todo, ws, () => {
305
- // SUNSET: dot_stuffing was renamed to dot_stuffed, remove it after 2026-01
306
309
  todo.message_stream.pipe(ws, {
307
- dot_stuffing: true,
308
310
  dot_stuffed: false,
309
311
  })
310
312
  })