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.
- package/.prettierignore +2 -0
- package/CONTRIBUTORS.md +24 -2
- package/Changes.md +48 -0
- package/Plugins.md +81 -64
- package/README.md +1 -1
- package/bin/haraka +9 -7
- package/config/connection.ini +10 -0
- package/config/smtp.ini +0 -9
- package/connection.js +15 -19
- package/docs/CoreConfig.md +2 -3
- package/docs/Plugins.md +1 -1
- package/docs/plugins/aliases.md +0 -2
- package/docs/plugins/queue/qmail-queue.md +0 -1
- package/docs/tutorials/Migrating_from_v1_to_v2.md +1 -1
- package/logger.js +2 -2
- package/outbound/client_pool.js +1 -1
- package/outbound/hmail.js +76 -83
- package/outbound/index.js +36 -34
- package/outbound/queue.js +231 -176
- package/package.json +29 -31
- package/plugins/prevent_credential_leaks.js +2 -2
- package/plugins/process_title.js +1 -1
- package/plugins/queue/smtp_forward.js +1 -1
- package/plugins/status.js +8 -5
- package/plugins/tls.js +1 -1
- package/plugins.js +19 -14
- package/rfc1869.js +10 -10
- package/run_tests +20 -2
- package/server.js +15 -10
- package/smtp_client.js +2 -9
- package/test/config/tls/haraka.local.pem +47 -47
- package/test/connection.js +286 -147
- package/test/fixtures/line_socket.js +1 -0
- package/test/fixtures/util_hmailitem.js +1 -1
- package/test/outbound/bounce_net_errors.js +176 -0
- package/test/outbound/bounce_rfc3464.js +303 -0
- package/test/outbound/hmail.js +140 -104
- package/test/outbound/index.js +61 -101
- package/test/outbound/qfile.js +25 -25
- package/test/outbound/queue.js +233 -0
- package/test/plugins/queue/smtp_forward.js +1 -1
- package/test/plugins/record_envelope_addresses.js +93 -0
- package/test/plugins/tls.js +2 -2
- package/test/plugins/xclient.js +137 -0
- package/test/rfc1869.js +43 -0
- package/test/smtp_client.js +6 -6
- package/test/transaction.js +486 -201
- package/tls_socket.js +3 -3
- package/transaction.js +33 -10
- package/config/me +0 -1
- package/config/rabbitmq.ini +0 -10
- package/config/rabbitmq_amqplib.ini +0 -19
- package/config/tls_cert.pem +0 -23
- package/config/tls_key.pem +0 -28
- package/docs/plugins/queue/rabbitmq.md +0 -34
- package/docs/plugins/queue/rabbitmq_amqplib.md +0 -51
- package/plugins/queue/rabbitmq.js +0 -141
- package/plugins/queue/rabbitmq_amqplib.js +0 -96
- package/test/config/tls/ec.pem +0 -23
- package/test/config/tls/mismatched.pem +0 -49
- package/test/outbound_bounce_net_errors.js +0 -157
- package/test/outbound_bounce_rfc3464.js +0 -366
- package/test/queue/multibyte +0 -0
- package/test/queue/plain +0 -0
- package/test/test-queue/delete-me +0 -0
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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')
|
package/docs/CoreConfig.md
CHANGED
|
@@ -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.
|
|
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
|
package/docs/plugins/aliases.md
CHANGED
|
@@ -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
|
|
@@ -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
|
|
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 =
|
|
258
|
+
logobj = { ...logobj, ...data }
|
|
259
259
|
} else if (logger.format === logger.formats.JSON && data.constructor === Object) {
|
|
260
|
-
logobj =
|
|
260
|
+
logobj = { ...logobj, ...data }
|
|
261
261
|
} else if (Object.hasOwn(data, 'uuid')) {
|
|
262
262
|
// outbound/client_pool
|
|
263
263
|
logobj.uuid = data.uuid
|
package/outbound/client_pool.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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 }
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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.
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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.
|
|
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
|
-
|
|
1371
|
-
|
|
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
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
})
|