Haraka 3.1.3 → 3.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +2 -0
- package/CONTRIBUTORS.md +23 -1
- package/Changes.md +52 -0
- package/Plugins.md +81 -64
- package/README.md +1 -1
- package/bin/haraka +7 -5
- package/connection.js +15 -19
- package/docs/Plugins.md +1 -1
- package/docs/plugins/aliases.md +0 -2
- package/docs/plugins/queue/qmail-queue.md +0 -1
- package/logger.js +2 -2
- package/outbound/hmail.js +76 -83
- package/outbound/index.js +36 -34
- package/outbound/queue.js +231 -176
- package/package.json +26 -29
- package/plugins/prevent_credential_leaks.js +2 -2
- package/plugins/process_title.js +1 -1
- package/plugins/queue/smtp_forward.js +5 -5
- 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 +8 -2
- package/server.js +15 -10
- package/smtp_client.js +10 -15
- package/test/config/tls/haraka.local.pem +47 -47
- package/test/connection.js +286 -147
- package/test/endpoint.js +5 -4
- package/test/fixtures/line_socket.js +1 -0
- package/test/fixtures/util_hmailitem.js +1 -1
- package/test/host_pool.js +57 -31
- package/test/logger.js +75 -135
- package/test/outbound/bounce_net_errors.js +132 -0
- package/test/outbound/bounce_rfc3464.js +226 -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/auth/auth_base.js +39 -44
- package/test/plugins/auth/auth_vpopmaild.js +8 -9
- package/test/plugins/queue/smtp_forward.js +953 -183
- package/test/plugins/rcpt_to.host_list_base.js +58 -93
- package/test/plugins/rcpt_to.in_host_list.js +126 -175
- package/test/plugins/record_envelope_addresses.js +93 -0
- package/test/plugins/status.js +10 -10
- package/test/plugins/tls.js +11 -21
- package/test/plugins/xclient.js +102 -0
- package/test/plugins.js +10 -13
- package/test/rfc1869.js +71 -48
- package/test/server.js +281 -436
- package/test/smtp_client.js +1194 -220
- package/test/tls_socket.js +74 -243
- package/test/transaction.js +486 -201
- package/tls_socket.js +19 -23
- package/transaction.js +33 -10
- package/config/rabbitmq.ini +0 -10
- package/config/rabbitmq_amqplib.ini +0 -19
- 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/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/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
|
})
|