haraka 0.0.33 → 3.3.0
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/.claude/settings.local.json +28 -0
- package/.githooks/pre-commit +41 -0
- package/.prettierignore +6 -0
- package/.qlty/.gitignore +7 -0
- package/.qlty/configs/.shellcheckrc +1 -0
- package/.qlty/qlty.toml +15 -0
- package/CHANGELOG.md +1894 -0
- package/CLAUDE.md +40 -0
- package/CONTRIBUTORS.md +34 -0
- package/Dockerfile +50 -0
- package/GEMINI.md +38 -0
- package/LICENSE +22 -0
- package/Plugins.md +227 -0
- package/README.md +119 -4
- package/SECURITY.md +178 -0
- package/TODO +22 -0
- package/address.js +53 -0
- package/bin/haraka +593 -0
- package/bin/haraka_grep +32 -0
- package/config/aliases +2 -0
- package/config/auth_flat_file.ini +7 -0
- package/config/auth_vpopmaild.ini +9 -0
- package/config/connection.ini +79 -0
- package/config/delay_deny.ini +7 -0
- package/config/dhparams.pem +8 -0
- package/config/host_list +3 -0
- package/config/host_list_regex +6 -0
- package/config/http.ini +11 -0
- package/config/lmtp.ini +7 -0
- package/config/log.ini +11 -0
- package/config/me +1 -0
- package/config/outbound.bounce_message +18 -0
- package/config/outbound.bounce_message_html +36 -0
- package/config/outbound.bounce_message_image +106 -0
- package/config/outbound.ini +24 -0
- package/config/plugins +67 -0
- package/config/smtp.ini +37 -0
- package/config/smtp_bridge.ini +4 -0
- package/config/smtp_forward.ini +31 -0
- package/config/smtp_proxy.ini +27 -0
- package/config/tarpit.timeout +1 -0
- package/config/tls.ini +83 -0
- package/config/tls_cert.pem +23 -0
- package/config/tls_key.pem +28 -0
- package/config/watch.ini +12 -0
- package/config/xclient.hosts +2 -0
- package/connection.js +1863 -0
- package/contrib/Haraka.cf +6 -0
- package/contrib/Haraka.pm +35 -0
- package/contrib/bad_smtp_server.pl +25 -0
- package/contrib/bsd-rc.d/haraka +61 -0
- package/contrib/debian-init.d/haraka +87 -0
- package/contrib/haraka.init +96 -0
- package/contrib/haraka.service +23 -0
- package/contrib/plugin2npm.sh +81 -0
- package/contrib/ubuntu-upstart/haraka.conf +27 -0
- package/coverage/coverage-final.json +2 -0
- package/coverage/coverage-summary.json +33 -0
- package/coverage/tmp/coverage-79131-1779241025146-0.json +1 -0
- package/coverage/tmp/coverage-79132-1779240999690-0.json +1 -0
- package/coverage/tmp/coverage-79172-1779241000095-0.json +1 -0
- package/coverage/tmp/coverage-79210-1779241000156-0.json +1 -0
- package/coverage/tmp/coverage-79211-1779241000209-0.json +1 -0
- package/coverage/tmp/coverage-79212-1779241000266-0.json +1 -0
- package/coverage/tmp/coverage-79213-1779241000441-0.json +1 -0
- package/coverage/tmp/coverage-79214-1779241000626-0.json +1 -0
- package/coverage/tmp/coverage-79215-1779241000795-0.json +1 -0
- package/coverage/tmp/coverage-79216-1779241000965-0.json +1 -0
- package/coverage/tmp/coverage-79218-1779241001013-0.json +1 -0
- package/coverage/tmp/coverage-79219-1779241001179-0.json +1 -0
- package/coverage/tmp/coverage-79220-1779241006249-0.json +1 -0
- package/coverage/tmp/coverage-79227-1779241011453-0.json +1 -0
- package/coverage/tmp/coverage-79229-1779241011537-0.json +1 -0
- package/coverage/tmp/coverage-79230-1779241011647-0.json +1 -0
- package/coverage/tmp/coverage-79231-1779241011765-0.json +1 -0
- package/coverage/tmp/coverage-79232-1779241011841-0.json +1 -0
- package/coverage/tmp/coverage-79233-1779241011909-0.json +1 -0
- package/coverage/tmp/coverage-79234-1779241011984-0.json +1 -0
- package/coverage/tmp/coverage-79235-1779241012055-0.json +1 -0
- package/coverage/tmp/coverage-79236-1779241012230-0.json +1 -0
- package/coverage/tmp/coverage-79237-1779241012300-0.json +1 -0
- package/coverage/tmp/coverage-79238-1779241012368-0.json +1 -0
- package/coverage/tmp/coverage-79239-1779241012438-0.json +1 -0
- package/coverage/tmp/coverage-79240-1779241012511-0.json +1 -0
- package/coverage/tmp/coverage-79241-1779241012582-0.json +1 -0
- package/coverage/tmp/coverage-79242-1779241012652-0.json +1 -0
- package/coverage/tmp/coverage-79243-1779241012814-0.json +1 -0
- package/coverage/tmp/coverage-79244-1779241012931-0.json +1 -0
- package/coverage/tmp/coverage-79245-1779241013007-0.json +1 -0
- package/coverage/tmp/coverage-79246-1779241013106-0.json +1 -0
- package/coverage/tmp/coverage-79247-1779241013178-0.json +1 -0
- package/coverage/tmp/coverage-79248-1779241013244-0.json +1 -0
- package/coverage/tmp/coverage-79249-1779241013409-0.json +1 -0
- package/coverage/tmp/coverage-79250-1779241013697-0.json +1 -0
- package/coverage/tmp/coverage-79251-1779241013847-0.json +1 -0
- package/coverage/tmp/coverage-79252-1779241014288-0.json +1 -0
- package/coverage/tmp/coverage-79253-1779241014378-0.json +1 -0
- package/coverage/tmp/coverage-79254-1779241014428-0.json +1 -0
- package/coverage/tmp/coverage-79255-1779241021774-0.json +1 -0
- package/coverage/tmp/coverage-80382-1779241021949-0.json +1 -0
- package/coverage/tmp/coverage-80383-1779241025019-0.json +1 -0
- package/coverage/tmp/coverage-80384-1779241025133-0.json +1 -0
- package/docs/Body.md +1 -0
- package/docs/Config.md +1 -0
- package/docs/Connection.md +153 -0
- package/docs/CoreConfig.md +96 -0
- package/docs/CustomReturnCodes.md +3 -0
- package/docs/HAProxy.md +62 -0
- package/docs/Header.md +1 -0
- package/docs/Logging.md +129 -0
- package/docs/Outbound.md +210 -0
- package/docs/Plugins.md +372 -0
- package/docs/Results.md +7 -0
- package/docs/Transaction.md +135 -0
- package/docs/Tutorial.md +183 -0
- package/docs/deprecated/access.md +3 -0
- package/docs/deprecated/backscatterer.md +9 -0
- package/docs/deprecated/connect.rdns_access.md +53 -0
- package/docs/deprecated/data.headers.md +3 -0
- package/docs/deprecated/data.nomsgid.md +7 -0
- package/docs/deprecated/data.noreceived.md +11 -0
- package/docs/deprecated/data.rfc5322_header_checks.md +11 -0
- package/docs/deprecated/dkim_sign.md +97 -0
- package/docs/deprecated/dkim_verify.md +28 -0
- package/docs/deprecated/dnsbl.md +80 -0
- package/docs/deprecated/dnswl.md +73 -0
- package/docs/deprecated/lookup_rdns.strict.md +67 -0
- package/docs/deprecated/mail_from.access.md +52 -0
- package/docs/deprecated/mail_from.blocklist.md +18 -0
- package/docs/deprecated/mail_from.nobounces.md +8 -0
- package/docs/deprecated/rcpt_to.access.md +53 -0
- package/docs/deprecated/rcpt_to.blocklist.md +18 -0
- package/docs/deprecated/rcpt_to.routes.md +3 -0
- package/docs/deprecated/rdns.regexp.md +30 -0
- package/docs/plugins/aliases.md +3 -0
- package/docs/plugins/auth/auth_bridge.md +34 -0
- package/docs/plugins/auth/auth_ldap.md +4 -0
- package/docs/plugins/auth/auth_proxy.md +36 -0
- package/docs/plugins/auth/auth_vpopmaild.md +33 -0
- package/docs/plugins/auth/flat_file.md +40 -0
- package/docs/plugins/block_me.md +18 -0
- package/docs/plugins/data.signatures.md +11 -0
- package/docs/plugins/delay_deny.md +23 -0
- package/docs/plugins/max_unrecognized_commands.md +6 -0
- package/docs/plugins/prevent_credential_leaks.md +22 -0
- package/docs/plugins/process_title.md +42 -0
- package/docs/plugins/queue/deliver.md +3 -0
- package/docs/plugins/queue/discard.md +32 -0
- package/docs/plugins/queue/lmtp.md +24 -0
- package/docs/plugins/queue/qmail-queue.md +16 -0
- package/docs/plugins/queue/quarantine.md +87 -0
- package/docs/plugins/queue/smtp_bridge.md +32 -0
- package/docs/plugins/queue/smtp_forward.md +127 -0
- package/docs/plugins/queue/smtp_proxy.md +68 -0
- package/docs/plugins/queue/test.md +7 -0
- package/docs/plugins/rcpt_to.in_host_list.md +34 -0
- package/docs/plugins/rcpt_to.max_count.md +3 -0
- package/docs/plugins/record_envelope_addresses.md +20 -0
- package/docs/plugins/relay.md +3 -0
- package/docs/plugins/reseed_rng.md +16 -0
- package/docs/plugins/status.md +41 -0
- package/docs/plugins/tarpit.md +50 -0
- package/docs/plugins/tls.md +235 -0
- package/docs/plugins/toobusy.md +27 -0
- package/docs/plugins/xclient.md +10 -0
- package/docs/tutorials/Migrating_from_v1_to_v2.md +96 -0
- package/docs/tutorials/SettingUpOutbound.md +62 -0
- package/eslint.config.mjs +2 -0
- package/haraka.js +74 -0
- package/haraka.sh +2 -0
- package/http/html/404.html +58 -0
- package/http/html/index.html +47 -0
- package/http/package.json +21 -0
- package/line_socket.js +24 -0
- package/logger.js +322 -0
- package/outbound/client_pool.js +59 -0
- package/outbound/config.js +134 -0
- package/outbound/hmail.js +1504 -0
- package/outbound/index.js +349 -0
- package/outbound/qfile.js +93 -0
- package/outbound/queue.js +399 -0
- package/outbound/tls.js +85 -0
- package/outbound/todo.js +17 -0
- package/package.json +99 -4
- package/plugins/.eslintrc.yaml +3 -0
- package/plugins/auth/auth_base.js +261 -0
- package/plugins/auth/auth_bridge.js +20 -0
- package/plugins/auth/auth_proxy.js +227 -0
- package/plugins/auth/auth_vpopmaild.js +162 -0
- package/plugins/auth/flat_file.js +44 -0
- package/plugins/block_me.js +88 -0
- package/plugins/data.signatures.js +30 -0
- package/plugins/delay_deny.js +153 -0
- package/plugins/prevent_credential_leaks.js +61 -0
- package/plugins/process_title.js +197 -0
- package/plugins/profile.js +11 -0
- package/plugins/queue/deliver.js +12 -0
- package/plugins/queue/discard.js +27 -0
- package/plugins/queue/lmtp.js +45 -0
- package/plugins/queue/qmail-queue.js +93 -0
- package/plugins/queue/quarantine.js +133 -0
- package/plugins/queue/smtp_bridge.js +45 -0
- package/plugins/queue/smtp_forward.js +371 -0
- package/plugins/queue/smtp_proxy.js +142 -0
- package/plugins/queue/test.js +15 -0
- package/plugins/rcpt_to.host_list_base.js +65 -0
- package/plugins/rcpt_to.in_host_list.js +56 -0
- package/plugins/record_envelope_addresses.js +17 -0
- package/plugins/reseed_rng.js +7 -0
- package/plugins/status.js +274 -0
- package/plugins/tarpit.js +45 -0
- package/plugins/tls.js +164 -0
- package/plugins/toobusy.js +47 -0
- package/plugins/xclient.js +124 -0
- package/plugins.js +604 -0
- package/queue/1772642154987_1775581346001_4_82235_TGwgfd_2_mattbook-m3.home.simerson.net +0 -0
- package/run_tests +11 -0
- package/server.js +827 -0
- package/smtp_client.js +504 -0
- package/test/.eslintrc.yaml +11 -0
- package/test/config/auth_flat_file.ini +5 -0
- package/test/config/block_me.recipient +1 -0
- package/test/config/block_me.senders +1 -0
- package/test/config/dhparams.pem +8 -0
- package/test/config/host_list +2 -0
- package/test/config/outbound_tls_cert.pem +1 -0
- package/test/config/outbound_tls_key.pem +1 -0
- package/test/config/plugins +7 -0
- package/test/config/smtp.ini +11 -0
- package/test/config/smtp_forward.ini +30 -0
- package/test/config/tls/example.com/_.example.com.key +28 -0
- package/test/config/tls/example.com/example.com.crt +25 -0
- package/test/config/tls/haraka.local.pem +51 -0
- package/test/config/tls.ini +45 -0
- package/test/config/tls_cert.pem +21 -0
- package/test/config/tls_key.pem +28 -0
- package/test/connection.js +817 -0
- package/test/fixtures/haproxy_allowed/config/connection.ini +3 -0
- package/test/fixtures/haproxy_disabled/config/connection.ini +3 -0
- package/test/fixtures/haproxy_untrusted/config/connection.ini +3 -0
- package/test/fixtures/line_socket.js +21 -0
- package/test/fixtures/todo_qfile.txt +0 -0
- package/test/fixtures/util_hmailitem.js +156 -0
- package/test/installation/config/test-plugin-flat +1 -0
- package/test/installation/config/test-plugin.ini +10 -0
- package/test/installation/config/tls.ini +1 -0
- package/test/installation/node_modules/load_first/index.js +5 -0
- package/test/installation/node_modules/load_first/package.json +11 -0
- package/test/installation/node_modules/test-plugin/config/test-plugin-flat +1 -0
- package/test/installation/node_modules/test-plugin/config/test-plugin.ini +9 -0
- package/test/installation/node_modules/test-plugin/package.json +5 -0
- package/test/installation/node_modules/test-plugin/test-plugin.js +5 -0
- package/test/installation/plugins/base_plugin.js +3 -0
- package/test/installation/plugins/folder_plugin/index.js +3 -0
- package/test/installation/plugins/folder_plugin/package.json +11 -0
- package/test/installation/plugins/inherits.js +7 -0
- package/test/installation/plugins/load_first.js +3 -0
- package/test/installation/plugins/plugin.js +1 -0
- package/test/installation/plugins/tls.js +3 -0
- package/test/logger.js +217 -0
- package/test/loud/config/dhparams.pem +0 -0
- package/test/loud/config/tls/goobered.pem +45 -0
- package/test/loud/config/tls.ini +43 -0
- package/test/mail_specimen/base64-root-part.txt +23 -0
- package/test/mail_specimen/varied-fold-lengths-preserve-data.txt +283 -0
- package/test/outbound/bounce_net_errors.js +133 -0
- package/test/outbound/bounce_rfc3464.js +226 -0
- package/test/outbound/hmail.js +210 -0
- package/test/outbound/index.js +385 -0
- package/test/outbound/qfile.js +124 -0
- package/test/outbound/queue.js +325 -0
- package/test/plugins/auth/auth_base.js +620 -0
- package/test/plugins/auth/auth_bridge.js +80 -0
- package/test/plugins/auth/auth_vpopmaild.js +81 -0
- package/test/plugins/auth/flat_file.js +123 -0
- package/test/plugins/block_me.js +141 -0
- package/test/plugins/data.signatures.js +111 -0
- package/test/plugins/delay_deny.js +262 -0
- package/test/plugins/prevent_credential_leaks.js +174 -0
- package/test/plugins/process_title.js +141 -0
- package/test/plugins/queue/deliver.js +98 -0
- package/test/plugins/queue/discard.js +78 -0
- package/test/plugins/queue/lmtp.js +137 -0
- package/test/plugins/queue/qmail-queue.js +98 -0
- package/test/plugins/queue/quarantine.js +80 -0
- package/test/plugins/queue/smtp_bridge.js +152 -0
- package/test/plugins/queue/smtp_forward.js +1023 -0
- package/test/plugins/queue/smtp_proxy.js +138 -0
- package/test/plugins/rcpt_to.host_list_base.js +102 -0
- package/test/plugins/rcpt_to.in_host_list.js +186 -0
- package/test/plugins/record_envelope_addresses.js +66 -0
- package/test/plugins/reseed_rng.js +34 -0
- package/test/plugins/status.js +207 -0
- package/test/plugins/tarpit.js +90 -0
- package/test/plugins/tls.js +86 -0
- package/test/plugins/toobusy.js +21 -0
- package/test/plugins/xclient.js +119 -0
- package/test/plugins.js +230 -0
- package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_fixed +0 -0
- package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka +0 -0
- package/test/queue/1508269674999_1508269674999_0_34002_socVUF_1_haraka +0 -0
- package/test/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka +0 -0
- package/test/queue/zero-length +0 -0
- package/test/server.js +1012 -0
- package/test/smtp_client.js +1303 -0
- package/test/tls_socket.js +321 -0
- package/test/transaction.js +554 -0
- package/tls_socket.js +771 -0
- package/transaction.js +267 -0
|
@@ -0,0 +1,1504 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const events = require('node:events')
|
|
4
|
+
const fs = require('node:fs/promises')
|
|
5
|
+
const { createReadStream } = require('node:fs')
|
|
6
|
+
const dns = require('node:dns')
|
|
7
|
+
const net = require('node:net')
|
|
8
|
+
const path = require('node:path')
|
|
9
|
+
|
|
10
|
+
const { Address } = require('../address')
|
|
11
|
+
const config = require('haraka-config')
|
|
12
|
+
const constants = require('haraka-constants')
|
|
13
|
+
const DSN = require('haraka-dsn')
|
|
14
|
+
const message = require('haraka-email-message')
|
|
15
|
+
const net_utils = require('haraka-net-utils')
|
|
16
|
+
const Notes = require('haraka-notes')
|
|
17
|
+
const utils = require('haraka-utils')
|
|
18
|
+
|
|
19
|
+
const logger = require('../logger')
|
|
20
|
+
const plugins = require('../plugins')
|
|
21
|
+
|
|
22
|
+
const client_pool = require('./client_pool')
|
|
23
|
+
const _qfile = require('./qfile')
|
|
24
|
+
const outbound = require('./index')
|
|
25
|
+
const obtls = require('./tls')
|
|
26
|
+
|
|
27
|
+
const FsyncWriteStream = utils.FsyncWriteStream
|
|
28
|
+
|
|
29
|
+
let queue_dir
|
|
30
|
+
let temp_fail_queue
|
|
31
|
+
let delivery_queue
|
|
32
|
+
setImmediate(() => {
|
|
33
|
+
const queuelib = require('./queue')
|
|
34
|
+
queue_dir = queuelib.queue_dir
|
|
35
|
+
temp_fail_queue = queuelib.temp_fail_queue
|
|
36
|
+
delivery_queue = queuelib.delivery_queue
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const obc = require('./config')
|
|
40
|
+
|
|
41
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
42
|
+
// HMailItem - encapsulates an individual outbound mail item
|
|
43
|
+
|
|
44
|
+
function dummy_func() {}
|
|
45
|
+
|
|
46
|
+
class HMailItem extends events.EventEmitter {
|
|
47
|
+
constructor(filename, filePath, notes) {
|
|
48
|
+
super()
|
|
49
|
+
|
|
50
|
+
const parts = _qfile.parts(filename)
|
|
51
|
+
if (!parts) throw new Error(`Bad filename: ${filename}`)
|
|
52
|
+
|
|
53
|
+
this.cfg = obc.cfg
|
|
54
|
+
this.obtls = obtls
|
|
55
|
+
this.name = 'outbound'
|
|
56
|
+
this.path = filePath
|
|
57
|
+
this.filename = filename
|
|
58
|
+
this.next_process = parts.next_attempt
|
|
59
|
+
this.num_failures = parts.attempts
|
|
60
|
+
this.pid = parts.pid
|
|
61
|
+
this.notes = notes || new Notes()
|
|
62
|
+
this.refcount = 1
|
|
63
|
+
this.todo = null
|
|
64
|
+
this.file_size = 0
|
|
65
|
+
this.next_cb = dummy_func
|
|
66
|
+
this.bounce_error = null
|
|
67
|
+
this.hook = null
|
|
68
|
+
this.size_file()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
data_stream() {
|
|
72
|
+
return createReadStream(this.path, {
|
|
73
|
+
start: this.data_start,
|
|
74
|
+
end: this.file_size,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async size_file() {
|
|
79
|
+
try {
|
|
80
|
+
const stats = await fs.stat(this.path)
|
|
81
|
+
if (stats.size === 0) {
|
|
82
|
+
this.logerror(`Error reading queue file ${this.filename}: zero bytes`)
|
|
83
|
+
this.emit('error', `Error reading queue file ${this.filename}: zero bytes`)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.file_size = stats.size
|
|
88
|
+
this.read_todo()
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// The file is unreadable (deleted, permissions, I/O error) and this.todo
|
|
91
|
+
// is still null, so there is no sender to bounce to. Release the queue slot.
|
|
92
|
+
this.logerror(`Error obtaining file size: ${err}`)
|
|
93
|
+
this.next_cb()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async read_todo() {
|
|
98
|
+
try {
|
|
99
|
+
const bytes = await this._stream_bytes_from(this.path, { start: 0, end: 3 })
|
|
100
|
+
|
|
101
|
+
const todo_len = bytes.readUInt32BE(0)
|
|
102
|
+
this.logdebug(`todo header length: ${todo_len}`)
|
|
103
|
+
this.data_start = todo_len + 4
|
|
104
|
+
|
|
105
|
+
const todo_bytes = await this._stream_bytes_from(this.path, { start: 4, end: todo_len + 3 })
|
|
106
|
+
if (todo_bytes.length !== todo_len) {
|
|
107
|
+
const wrongLength = `Didn't find right amount of data in todo: ${this.path}`
|
|
108
|
+
this.logcrit(wrongLength)
|
|
109
|
+
try {
|
|
110
|
+
await fs.rename(this.path, path.join(queue_dir, `error.${this.filename}`))
|
|
111
|
+
} catch (renameErr) {
|
|
112
|
+
this.logerror(`Failed to move corrupt todo file ${this.path} to error queue: ${renameErr}`)
|
|
113
|
+
}
|
|
114
|
+
this.emit('error', wrongLength) // Note nothing picks this up yet
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// we read everything
|
|
119
|
+
const todo_json = todo_bytes.toString().trim()
|
|
120
|
+
const last_char = todo_json.charAt(todo_json.length - 1)
|
|
121
|
+
if (last_char !== '}') {
|
|
122
|
+
this.emit('error', `invalid todo header end char: ${last_char} at pos ${todo_len} of ${this.filename}`)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
this.todo = JSON.parse(todo_json)
|
|
126
|
+
this.todo.mail_from = new Address(this.todo.mail_from)
|
|
127
|
+
this.todo.rcpt_to = this.todo.rcpt_to.map((a) => new Address(a))
|
|
128
|
+
this.todo.notes = new Notes(this.todo.notes)
|
|
129
|
+
this.emit('ready')
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const errMsg = `Error reading queue file ${this.filename}: ${err}`
|
|
132
|
+
this.logerror(errMsg)
|
|
133
|
+
this.temp_fail(errMsg)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_stream_bytes_from(file_path, opts) {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
if (opts.encoding !== undefined) {
|
|
140
|
+
// passing an encoding to fs.createReadStream will change the type of data returned
|
|
141
|
+
// ex: instead of returning a buffer, it may return a String, which will cause
|
|
142
|
+
// Buffer.concat to barf. There's a reason this function has 'bytes' in the name
|
|
143
|
+
reject(new Error('Thar be dragons here! Encode/decode on the result of this function'))
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
const stream = createReadStream(file_path, opts)
|
|
147
|
+
stream.on('error', reject)
|
|
148
|
+
|
|
149
|
+
let raw_bytes = Buffer.alloc(0)
|
|
150
|
+
stream.on('data', (data) => {
|
|
151
|
+
raw_bytes = Buffer.concat([raw_bytes, data])
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
stream.on('end', () => {
|
|
155
|
+
resolve(raw_bytes)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
send() {
|
|
161
|
+
if (obc.cfg.disabled) {
|
|
162
|
+
// try again in 1 second if delivery is disabled
|
|
163
|
+
this.logdebug('delivery disabled temporarily. Retrying in 1s.')
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
this.send()
|
|
166
|
+
}, 1000)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!this.todo) {
|
|
171
|
+
this.once('ready', () => {
|
|
172
|
+
this._send()
|
|
173
|
+
})
|
|
174
|
+
} else {
|
|
175
|
+
this._send()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_send() {
|
|
180
|
+
plugins.run_hooks('send_email', this)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
send_email_respond(retval, delay_seconds) {
|
|
184
|
+
if (retval === constants.delay) {
|
|
185
|
+
// Try again in 'delay' seconds.
|
|
186
|
+
this.logdebug(`Delivery of this email delayed for ${delay_seconds} seconds`)
|
|
187
|
+
this.next_cb()
|
|
188
|
+
temp_fail_queue.add(this.filename, delay_seconds * 1000, () => {
|
|
189
|
+
delivery_queue.push(this)
|
|
190
|
+
})
|
|
191
|
+
} else {
|
|
192
|
+
this.logdebug(`Sending mail: ${this.filename}`)
|
|
193
|
+
this.get_mx()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
get_mx() {
|
|
198
|
+
const { domain } = this.todo
|
|
199
|
+
plugins.run_hooks('get_mx', this, domain)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async get_mx_respond(retval, mx) {
|
|
203
|
+
switch (retval) {
|
|
204
|
+
case constants.ok: {
|
|
205
|
+
this.logdebug(`MX from Plugin: ${this.todo.domain} => 0 ${JSON.stringify(mx)}`)
|
|
206
|
+
let mx_list
|
|
207
|
+
if (Array.isArray(mx)) {
|
|
208
|
+
mx_list = mx.map((m) => new net_utils.HarakaMx(m))
|
|
209
|
+
} else {
|
|
210
|
+
mx_list = [new net_utils.HarakaMx(mx)]
|
|
211
|
+
}
|
|
212
|
+
return this.found_mx(mx_list)
|
|
213
|
+
}
|
|
214
|
+
case constants.deny:
|
|
215
|
+
this.logwarn(`get_mx plugin returned DENY: ${mx}`)
|
|
216
|
+
for (const rcpt of this.todo.rcpt_to) {
|
|
217
|
+
this.extend_rcpt_with_dsn(rcpt, DSN.addr_bad_dest_system(`No MX for ${this.todo.domain}`))
|
|
218
|
+
}
|
|
219
|
+
return this.bounce(`No MX for ${this.todo.domain}`)
|
|
220
|
+
case constants.denysoft:
|
|
221
|
+
this.logwarn(`get_mx plugin returned DENYSOFT: ${mx}`)
|
|
222
|
+
for (const rcpt of this.todo.rcpt_to) {
|
|
223
|
+
this.extend_rcpt_with_dsn(
|
|
224
|
+
rcpt,
|
|
225
|
+
DSN.addr_bad_dest_system(`Temporary MX lookup error for ${this.todo.domain}`, 450),
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
return this.temp_fail(`Temporary MX lookup error for ${this.todo.domain}`)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// none of the above return codes, drop through to DNS
|
|
232
|
+
try {
|
|
233
|
+
const exchanges = await net_utils.get_mx(this.todo.domain)
|
|
234
|
+
|
|
235
|
+
if (exchanges.length) {
|
|
236
|
+
this.found_mx(this.sort_mx(exchanges))
|
|
237
|
+
} else {
|
|
238
|
+
for (const rcpt of this.todo.rcpt_to) {
|
|
239
|
+
this.extend_rcpt_with_dsn(
|
|
240
|
+
rcpt,
|
|
241
|
+
DSN.addr_bad_dest_system(`Nowhere to deliver mail to for domain: ${this.todo.domain}`),
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
this.bounce(`Nowhere to deliver mail to for domain: ${this.todo.domain}`)
|
|
245
|
+
}
|
|
246
|
+
} catch (e) {
|
|
247
|
+
this.get_mx_error(e)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
get_mx_error(err) {
|
|
252
|
+
this.lognotice(`MX Lookup for ${this.todo.domain} failed: ${err}`)
|
|
253
|
+
|
|
254
|
+
if (err.code === dns.NXDOMAIN || err.code === dns.NOTFOUND) {
|
|
255
|
+
for (const rcpt of this.todo.rcpt_to) {
|
|
256
|
+
this.extend_rcpt_with_dsn(rcpt, DSN.addr_bad_dest_system(`No Such Domain: ${this.todo.domain}`))
|
|
257
|
+
}
|
|
258
|
+
this.bounce(`No Such Domain: ${this.todo.domain}`)
|
|
259
|
+
} else {
|
|
260
|
+
// every other error is transient
|
|
261
|
+
for (const rcpt of this.todo.rcpt_to) {
|
|
262
|
+
this.extend_rcpt_with_dsn(rcpt, DSN.addr_unspecified(`DNS lookup failure: ${this.todo.domain}`))
|
|
263
|
+
}
|
|
264
|
+
this.temp_fail(`DNS lookup failure: ${err}`)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async found_mx(mxs) {
|
|
269
|
+
// support RFC 7505 null MX
|
|
270
|
+
if (mxs.length === 1 && mxs[0].priority === 0 && mxs[0].exchange === '') {
|
|
271
|
+
for (const rcpt of this.todo.rcpt_to) {
|
|
272
|
+
this.extend_rcpt_with_dsn(
|
|
273
|
+
rcpt,
|
|
274
|
+
DSN.addr_null_mx(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`),
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
return this.bounce(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// resolves the MX hostnames to IPs
|
|
281
|
+
this.mxlist = await net_utils.resolve_mx_hosts(mxs)
|
|
282
|
+
|
|
283
|
+
switch (obc.cfg.inet_prefer) {
|
|
284
|
+
case 'v4':
|
|
285
|
+
this.mxlist = [
|
|
286
|
+
...this.mxlist.filter((mx) => !net.isIP(mx.exchange) || net.isIPv4(mx.exchange)),
|
|
287
|
+
...this.mxlist.filter((mx) => net.isIPv6(mx.exchange)),
|
|
288
|
+
]
|
|
289
|
+
break
|
|
290
|
+
case 'v6':
|
|
291
|
+
this.mxlist = [
|
|
292
|
+
...this.mxlist.filter((mx) => !net.isIP(mx.exchange) || net.isIPv6(mx.exchange)),
|
|
293
|
+
...this.mxlist.filter((mx) => net.isIPv4(mx.exchange)),
|
|
294
|
+
]
|
|
295
|
+
break
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.try_deliver()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async try_deliver() {
|
|
302
|
+
// are any MXs left?
|
|
303
|
+
if (this.mxlist.length === 0) {
|
|
304
|
+
for (const rcpt of this.todo.rcpt_to) {
|
|
305
|
+
this.extend_rcpt_with_dsn(rcpt, DSN.addr_bad_dest_system(`Tried all MXs ${this.todo.domain}`))
|
|
306
|
+
}
|
|
307
|
+
return this.temp_fail('Tried all MXs')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const mx = this.mxlist.shift()
|
|
311
|
+
|
|
312
|
+
if (!obc.cfg.local_mx_ok && mx.from_dns && (await net_utils.is_local_host(mx.exchange))) {
|
|
313
|
+
this.loginfo(`MX ${mx.exchange} is local, skipping since local_mx_ok=false`)
|
|
314
|
+
return this.try_deliver() // try next MX
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.force_tls = this.get_force_tls(mx)
|
|
318
|
+
|
|
319
|
+
if (this.todo.notes.outbound_ip) {
|
|
320
|
+
this.logerror(`notes.outbound_ip is deprecated. Use get_mx.bind instead!`)
|
|
321
|
+
if (!mx.bind) mx.bind = this.todo.notes.outbound_ip
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Allow transaction notes to set outbound IP helo
|
|
325
|
+
if (this.todo.notes.outbound_helo) {
|
|
326
|
+
mx.bind_helo = this.todo.notes.outbound_helo
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const host = mx.path ? mx.path : mx.exchange
|
|
330
|
+
const lmtp = mx.using_lmtp ? ' using LMTP' : ''
|
|
331
|
+
if (!mx.port) mx.port = mx.using_lmtp ? 24 : 25
|
|
332
|
+
const from_dns = mx.from_dns ? ' (via DNS)' : ''
|
|
333
|
+
|
|
334
|
+
this.logdebug(
|
|
335
|
+
`deliver: ${mx.bind_helo} -> ${host}${lmtp}${from_dns} (${delivery_queue.length()}) (${temp_fail_queue.length()})`,
|
|
336
|
+
)
|
|
337
|
+
client_pool.get_client(mx, (err, socket) => {
|
|
338
|
+
if (err) {
|
|
339
|
+
if (/connection timed out|connect ECONNREFUSED/.test(err)) {
|
|
340
|
+
logger.notice(this, `Failed to get socket: ${err}`)
|
|
341
|
+
} else {
|
|
342
|
+
logger.error(this, `Failed to get socket: ${err}`)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return this.try_deliver() // try next MX
|
|
346
|
+
}
|
|
347
|
+
this.try_deliver_host_on_socket(mx, host, mx.port, socket)
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try_deliver_host_on_socket(mx, host, port, socket) {
|
|
352
|
+
const self = this
|
|
353
|
+
let processing_mail = true
|
|
354
|
+
let command = mx.using_lmtp ? 'connect_lmtp' : 'connect'
|
|
355
|
+
|
|
356
|
+
for (const l of ['error', 'timeout', 'close', 'end']) {
|
|
357
|
+
socket.removeAllListeners(l)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
socket.once('timeout', function () {
|
|
361
|
+
socket.emit('error', `socket timeout waiting on ${command}`)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
socket.on('error', (err) => {
|
|
365
|
+
if (!processing_mail) return
|
|
366
|
+
|
|
367
|
+
self.logerror(`Ongoing connection failed to ${host}:${port} : ${err}`)
|
|
368
|
+
processing_mail = false
|
|
369
|
+
client_pool.release_client(socket, mx)
|
|
370
|
+
if (err.source === 'tls')
|
|
371
|
+
// exception thrown from tls_socket during tls upgrade
|
|
372
|
+
return obtls.mark_tls_nogo(host, () => {
|
|
373
|
+
return self.try_deliver()
|
|
374
|
+
})
|
|
375
|
+
self.try_deliver() // try the next MX
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
socket.once('close', () => {
|
|
379
|
+
if (!processing_mail) return
|
|
380
|
+
|
|
381
|
+
self.logerror(`Remote end ${host}:${port} closed connection while we were processing mail. Trying next MX.`)
|
|
382
|
+
processing_mail = false
|
|
383
|
+
client_pool.release_client(socket, mx)
|
|
384
|
+
self.try_deliver()
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
socket.once('end', () => {
|
|
388
|
+
socket.writable = false
|
|
389
|
+
if (!processing_mail) client_pool.release_client(socket, mx)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
let response = []
|
|
393
|
+
|
|
394
|
+
let recip_index = 0
|
|
395
|
+
const recipients = this.todo.rcpt_to
|
|
396
|
+
let lmtp_rcpt_idx = 0
|
|
397
|
+
|
|
398
|
+
let last_recip = null
|
|
399
|
+
const ok_recips = []
|
|
400
|
+
const fail_recips = []
|
|
401
|
+
const bounce_recips = []
|
|
402
|
+
let secured = false
|
|
403
|
+
let authenticating = false
|
|
404
|
+
let authenticated = false
|
|
405
|
+
let smtp_properties = {
|
|
406
|
+
tls: false,
|
|
407
|
+
max_size: 0,
|
|
408
|
+
eightbitmime: false,
|
|
409
|
+
enh_status_codes: false,
|
|
410
|
+
auth: [],
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const send_command = (socket.send_command = (cmd, data) => {
|
|
414
|
+
if (!socket.writable) {
|
|
415
|
+
self.logerror('Socket writability went away')
|
|
416
|
+
if (processing_mail) {
|
|
417
|
+
processing_mail = false
|
|
418
|
+
client_pool.release_client(socket, mx)
|
|
419
|
+
return self.try_deliver()
|
|
420
|
+
}
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
if (self.force_tls && !['EHLO', 'LHLO', 'STARTTLS'].includes(cmd.toUpperCase()) && !socket.isSecure()) {
|
|
424
|
+
// For safety against programming mistakes
|
|
425
|
+
self.logerror(
|
|
426
|
+
'Blocking attempt to send unencrypted data to forced TLS socket. This message indicates a programming error in the software.',
|
|
427
|
+
)
|
|
428
|
+
processing_mail = false
|
|
429
|
+
client_pool.release_client(socket, mx)
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let line = `${cmd}${data ? ` ${data}` : ''}`
|
|
434
|
+
if (cmd === 'dot' || cmd === 'dot_lmtp') {
|
|
435
|
+
line = '.'
|
|
436
|
+
}
|
|
437
|
+
if (authenticating) cmd = 'auth'
|
|
438
|
+
self.logprotocol(`C: ${line}`)
|
|
439
|
+
socket.write(`${line}\r\n`, 'utf8', (err) => {
|
|
440
|
+
if (err) {
|
|
441
|
+
self.logcrit(`Socket write failed unexpectedly: ${err}`)
|
|
442
|
+
// We may want to release client here - but I want to get this
|
|
443
|
+
// line of code in before we do that so we might see some logging
|
|
444
|
+
// in case of errors.
|
|
445
|
+
// client_pool.release_client(socket, mx);
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
command = cmd.toLowerCase()
|
|
449
|
+
response = []
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
function set_ehlo_props() {
|
|
453
|
+
for (let i = 0, l = response.length; i < l; i++) {
|
|
454
|
+
const r = response[i]
|
|
455
|
+
if (r.toUpperCase() === '8BITMIME') {
|
|
456
|
+
smtp_properties.eightbitmime = true
|
|
457
|
+
} else if (r.toUpperCase() === 'STARTTLS') {
|
|
458
|
+
smtp_properties.tls = true
|
|
459
|
+
} else if (r.toUpperCase() === 'ENHANCEDSTATUSCODES') {
|
|
460
|
+
smtp_properties.enh_status_codes = true
|
|
461
|
+
} else if (r.toUpperCase() === 'SMTPUTF8') {
|
|
462
|
+
smtp_properties.smtputf8 = true
|
|
463
|
+
} else {
|
|
464
|
+
// Check for SIZE parameter and limit
|
|
465
|
+
let matches = r.match(/^SIZE\s+(\d+)$/)
|
|
466
|
+
if (matches) {
|
|
467
|
+
smtp_properties.max_size = matches[1]
|
|
468
|
+
}
|
|
469
|
+
// Check for AUTH
|
|
470
|
+
matches = r.match(/^AUTH\s+(.+)$/)
|
|
471
|
+
if (matches) {
|
|
472
|
+
smtp_properties.auth = matches[1].split(/\s+/)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function get_reverse_path_with_params() {
|
|
479
|
+
const rp = self.todo.mail_from.format(!smtp_properties.smtputf8)
|
|
480
|
+
let rp_params = ''
|
|
481
|
+
if (smtp_properties.smtputf8 && has_non_ascii(rp)) rp_params += ' SMTPUTF8'
|
|
482
|
+
return `FROM:${rp}${rp_params}`
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function has_non_ascii(string) {
|
|
486
|
+
return [...string].some((char) => char.charCodeAt(0) > 127)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function auth_and_mail_phase() {
|
|
490
|
+
if (!authenticated && mx.auth_user && mx.auth_pass) {
|
|
491
|
+
// We have AUTH credentials to send for this domain
|
|
492
|
+
|
|
493
|
+
if (!(Array.isArray(smtp_properties.auth) && smtp_properties.auth.length)) {
|
|
494
|
+
// AUTH not offered
|
|
495
|
+
self.logwarn(
|
|
496
|
+
`AUTH configured for domain ${self.todo.domain} but host ${host} did not advertise AUTH capability`,
|
|
497
|
+
)
|
|
498
|
+
// Try and send the message without authentication
|
|
499
|
+
return send_command('MAIL', get_reverse_path_with_params())
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!mx.auth_type) {
|
|
503
|
+
// User hasn't specified an authentication type, so we pick one
|
|
504
|
+
// We'll prefer CRAM-MD5 as it's the most secure that we support.
|
|
505
|
+
if (smtp_properties.auth.includes('CRAM-MD5')) {
|
|
506
|
+
mx.auth_type = 'CRAM-MD5'
|
|
507
|
+
}
|
|
508
|
+
// PLAIN requires less round-trips compared to LOGIN
|
|
509
|
+
else if (smtp_properties.auth.includes('PLAIN')) {
|
|
510
|
+
// PLAIN requires less round trips compared to LOGIN
|
|
511
|
+
// So we'll make this our 2nd pick.
|
|
512
|
+
mx.auth_type = 'PLAIN'
|
|
513
|
+
} else if (smtp_properties.auth.includes('LOGIN')) {
|
|
514
|
+
mx.auth_type = 'LOGIN'
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (!mx.auth_type || (mx.auth_type && !smtp_properties.auth.includes(mx.auth_type.toUpperCase()))) {
|
|
519
|
+
// No compatible authentication types offered by the server
|
|
520
|
+
self.logwarn(
|
|
521
|
+
`AUTH configured for domain ${self.todo.domain} but host ${host}did not offer any compatible types${mx.auth_type ? ` (requested: ${mx.auth_type})` : ''} (offered: ${smtp_properties.auth.join(',')})`,
|
|
522
|
+
)
|
|
523
|
+
// Proceed without authentication
|
|
524
|
+
return send_command('MAIL', get_reverse_path_with_params())
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
switch (mx.auth_type.toUpperCase()) {
|
|
528
|
+
case 'PLAIN':
|
|
529
|
+
return send_command('AUTH', `PLAIN ${utils.base64(`\0${mx.auth_user}\0${mx.auth_pass}`)}`)
|
|
530
|
+
case 'LOGIN':
|
|
531
|
+
authenticating = true
|
|
532
|
+
return send_command('AUTH', 'LOGIN')
|
|
533
|
+
case 'CRAM-MD5':
|
|
534
|
+
authenticating = true
|
|
535
|
+
return send_command('AUTH', 'CRAM-MD5')
|
|
536
|
+
default:
|
|
537
|
+
// Unsupported AUTH type
|
|
538
|
+
self.logwarn(
|
|
539
|
+
`Unsupported authentication type ${mx.auth_type.toUpperCase()} requested for domain ${self.todo.domain}`,
|
|
540
|
+
)
|
|
541
|
+
return send_command('MAIL', get_reverse_path_with_params())
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return send_command('MAIL', get_reverse_path_with_params())
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// IMPORTANT: do STARTTLS before AUTH for security
|
|
549
|
+
function process_ehlo_data() {
|
|
550
|
+
set_ehlo_props()
|
|
551
|
+
|
|
552
|
+
if (secured) return auth_and_mail_phase() // TLS already negotiated
|
|
553
|
+
|
|
554
|
+
if (self.force_tls) {
|
|
555
|
+
self.logdebug(`Using TLS for domain: ${self.todo.domain}, host: ${host}`)
|
|
556
|
+
|
|
557
|
+
if (!obc.cfg.enable_tls || !smtp_properties.tls) {
|
|
558
|
+
// Prevent further use of the non-securable socket
|
|
559
|
+
processing_mail = false
|
|
560
|
+
socket.write('QUIT\r\n', 'utf8') // courtesy
|
|
561
|
+
socket.end()
|
|
562
|
+
client_pool.release_client(socket, mx)
|
|
563
|
+
return self.temp_fail(`No TLS available but required by configuration.`)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
socket.once('secure', () => {
|
|
567
|
+
// Set this flag so we don't try STARTTLS again if it
|
|
568
|
+
// is incorrectly offered at EHLO once we are secured.
|
|
569
|
+
secured = true
|
|
570
|
+
send_command(mx.using_lmtp ? 'LHLO' : 'EHLO', mx.bind_helo)
|
|
571
|
+
})
|
|
572
|
+
return send_command('STARTTLS')
|
|
573
|
+
}
|
|
574
|
+
if (!obc.cfg.enable_tls) return auth_and_mail_phase() // TLS not enabled
|
|
575
|
+
if (!smtp_properties.tls) return auth_and_mail_phase() // TLS not advertised by remote
|
|
576
|
+
|
|
577
|
+
if (obtls.cfg === undefined) {
|
|
578
|
+
self.logerror(`Oops, TLS config not loaded yet!`)
|
|
579
|
+
return auth_and_mail_phase() // no outbound TLS config
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// TLS is configured and available
|
|
583
|
+
|
|
584
|
+
// TLS exclude lists checks for MX host or remote domain
|
|
585
|
+
if (net_utils.ip_in_list(obtls.cfg.no_tls_hosts, host)) return auth_and_mail_phase()
|
|
586
|
+
if (net_utils.ip_in_list(obtls.cfg.no_tls_hosts, self.todo.domain)) return auth_and_mail_phase()
|
|
587
|
+
|
|
588
|
+
// Check Redis and skip for hosts that failed past TLS upgrade
|
|
589
|
+
return obtls.check_tls_nogo(
|
|
590
|
+
host,
|
|
591
|
+
() => {
|
|
592
|
+
// Clear to GO
|
|
593
|
+
self.logdebug(`Trying TLS for domain: ${self.todo.domain}, host: ${host}`)
|
|
594
|
+
|
|
595
|
+
socket.once('secure', () => {
|
|
596
|
+
// Set this flag so we don't try STARTTLS again if it
|
|
597
|
+
// is incorrectly offered at EHLO once we are secured.
|
|
598
|
+
secured = true
|
|
599
|
+
send_command(mx.using_lmtp ? 'LHLO' : 'EHLO', mx.bind_helo)
|
|
600
|
+
})
|
|
601
|
+
return send_command('STARTTLS')
|
|
602
|
+
},
|
|
603
|
+
(when) => {
|
|
604
|
+
// No GO
|
|
605
|
+
self.loginfo(`TLS disabled for ${host} because it was marked as non-TLS on ${when}`)
|
|
606
|
+
return auth_and_mail_phase()
|
|
607
|
+
},
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
let fp_called = false
|
|
612
|
+
|
|
613
|
+
function finish_processing_mail(success) {
|
|
614
|
+
if (fp_called) {
|
|
615
|
+
return self.logerror(`finish_processing_mail called multiple times! Stack: ${new Error().stack}`)
|
|
616
|
+
}
|
|
617
|
+
fp_called = true
|
|
618
|
+
if (fail_recips.length) {
|
|
619
|
+
self.refcount++
|
|
620
|
+
self.split_to_new_recipients(fail_recips, 'Some recipients temporarily failed', (hmail) => {
|
|
621
|
+
self.discard()
|
|
622
|
+
hmail.temp_fail(`Some recipients temp failed: ${fail_recips.join(', ')}`, {
|
|
623
|
+
fail_recips,
|
|
624
|
+
mx,
|
|
625
|
+
})
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
if (bounce_recips.length) {
|
|
629
|
+
self.refcount++
|
|
630
|
+
self.split_to_new_recipients(bounce_recips, 'Some recipients rejected', (hmail) => {
|
|
631
|
+
self.discard()
|
|
632
|
+
hmail.bounce(`Some recipients failed: ${bounce_recips.join(', ')}`, {
|
|
633
|
+
bounce_recips,
|
|
634
|
+
mx,
|
|
635
|
+
})
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
processing_mail = false
|
|
639
|
+
if (success) {
|
|
640
|
+
const reason = response.join(' ')
|
|
641
|
+
|
|
642
|
+
let hostname = mx.exchange
|
|
643
|
+
if (net.isIP(hostname) && mx.from_dns && !net.isIP(mx.from_dns)) {
|
|
644
|
+
hostname = mx.from_dns
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
self.delivered(
|
|
648
|
+
host,
|
|
649
|
+
port,
|
|
650
|
+
mx.using_lmtp ? 'LMTP' : 'SMTP',
|
|
651
|
+
hostname,
|
|
652
|
+
reason,
|
|
653
|
+
ok_recips,
|
|
654
|
+
fail_recips,
|
|
655
|
+
bounce_recips,
|
|
656
|
+
secured,
|
|
657
|
+
authenticated,
|
|
658
|
+
)
|
|
659
|
+
} else {
|
|
660
|
+
self.discard()
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
send_command('QUIT')
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
socket.on('line', (line) => {
|
|
667
|
+
if (!processing_mail && command !== 'rset') {
|
|
668
|
+
if (command !== 'quit') {
|
|
669
|
+
self.logprotocol(`Received data after stopping processing: ${line}`)
|
|
670
|
+
}
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
self.logprotocol(`S: ${line}`)
|
|
674
|
+
const matches = smtp_regexp.exec(line)
|
|
675
|
+
if (!matches) {
|
|
676
|
+
// Unrecognized response.
|
|
677
|
+
self.logerror(`Unrecognized response from upstream server: ${line}`)
|
|
678
|
+
processing_mail = false
|
|
679
|
+
// Release back to the pool and instruct it to terminate this connection
|
|
680
|
+
client_pool.release_client(socket, mx)
|
|
681
|
+
for (const rcpt of self.todo.rcpt_to) {
|
|
682
|
+
self.extend_rcpt_with_dsn(
|
|
683
|
+
rcpt,
|
|
684
|
+
DSN.proto_invalid_command(`Unrecognized response from upstream server: ${line}`),
|
|
685
|
+
)
|
|
686
|
+
}
|
|
687
|
+
self.bounce(`Unrecognized response from upstream server: ${line}`, { mx })
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
let reason
|
|
692
|
+
const code = matches[1]
|
|
693
|
+
const cont = matches[2]
|
|
694
|
+
const extc = matches[3]
|
|
695
|
+
const rest = matches[4]
|
|
696
|
+
response.push(rest)
|
|
697
|
+
if (cont !== ' ') return
|
|
698
|
+
|
|
699
|
+
if (code.match(/^2/)) {
|
|
700
|
+
// Successful command, fall through
|
|
701
|
+
} else if (code.match(/^3/) && command !== 'data') {
|
|
702
|
+
if (authenticating) {
|
|
703
|
+
const resp = response.join(' ')
|
|
704
|
+
switch (mx.auth_type.toUpperCase()) {
|
|
705
|
+
case 'LOGIN':
|
|
706
|
+
if (resp === 'VXNlcm5hbWU6') {
|
|
707
|
+
// Username:
|
|
708
|
+
return send_command(utils.base64(mx.auth_user))
|
|
709
|
+
} else if (resp === 'UGFzc3dvcmQ6') {
|
|
710
|
+
// Password:
|
|
711
|
+
return send_command(utils.base64(mx.auth_pass))
|
|
712
|
+
}
|
|
713
|
+
break
|
|
714
|
+
case 'CRAM-MD5':
|
|
715
|
+
// The response is our challenge
|
|
716
|
+
return send_command(utils.cram_md5_response(mx.auth_user, mx.auth_pass, resp))
|
|
717
|
+
default:
|
|
718
|
+
// This shouldn't happen...
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
// Error
|
|
722
|
+
reason = response.join(' ')
|
|
723
|
+
for (const rcpt of recipients) {
|
|
724
|
+
rcpt.dsn_action = 'delayed'
|
|
725
|
+
rcpt.dsn_smtp_code = code
|
|
726
|
+
rcpt.dsn_smtp_extc = extc
|
|
727
|
+
rcpt.dsn_status = extc
|
|
728
|
+
rcpt.dsn_smtp_response = response.join(' ')
|
|
729
|
+
rcpt.dsn_remote_mta = mx.exchange
|
|
730
|
+
}
|
|
731
|
+
send_command('QUIT')
|
|
732
|
+
processing_mail = false
|
|
733
|
+
return self.temp_fail(`Upstream error: ${code} ${extc ? `${extc} ` : ''}${reason}`)
|
|
734
|
+
} else if (code.match(/^4/)) {
|
|
735
|
+
authenticating = false
|
|
736
|
+
if (/^rcpt/.test(command) || command === 'dot_lmtp') {
|
|
737
|
+
if (command === 'dot_lmtp') last_recip = ok_recips.shift()
|
|
738
|
+
// this recipient was rejected
|
|
739
|
+
reason = `${code} ${extc ? `${extc} ` : ''}${response.join(' ')}`
|
|
740
|
+
self.lognotice(`recipient ${last_recip} deferred: ${reason}`)
|
|
741
|
+
last_recip.reason = reason
|
|
742
|
+
|
|
743
|
+
last_recip.dsn_action = 'delayed'
|
|
744
|
+
last_recip.dsn_smtp_code = code
|
|
745
|
+
last_recip.dsn_smtp_extc = extc
|
|
746
|
+
last_recip.dsn_status = extc
|
|
747
|
+
last_recip.dsn_smtp_response = response.join(' ')
|
|
748
|
+
last_recip.dsn_remote_mta = mx.exchange
|
|
749
|
+
|
|
750
|
+
fail_recips.push(last_recip)
|
|
751
|
+
if (command === 'dot_lmtp') {
|
|
752
|
+
response = []
|
|
753
|
+
if (ok_recips.length === 0) {
|
|
754
|
+
return finish_processing_mail(false)
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} else if (processing_mail) {
|
|
758
|
+
reason = response.join(' ')
|
|
759
|
+
for (const rcpt of recipients) {
|
|
760
|
+
rcpt.dsn_action = 'delayed'
|
|
761
|
+
rcpt.dsn_smtp_code = code
|
|
762
|
+
rcpt.dsn_smtp_extc = extc
|
|
763
|
+
rcpt.dsn_status = extc
|
|
764
|
+
rcpt.dsn_smtp_response = response.join(' ')
|
|
765
|
+
rcpt.dsn_remote_mta = mx.exchange
|
|
766
|
+
}
|
|
767
|
+
send_command('QUIT')
|
|
768
|
+
processing_mail = false
|
|
769
|
+
return self.temp_fail(`Upstream error: ${code} ${extc ? `${extc} ` : ''}${reason}`)
|
|
770
|
+
} else {
|
|
771
|
+
reason = response.join(' ')
|
|
772
|
+
self.lognotice(`Error - but not processing mail: ${code} ${extc ? `${extc} ` : ''}${reason}`)
|
|
773
|
+
return client_pool.release_client(socket, mx)
|
|
774
|
+
}
|
|
775
|
+
} else if (code.match(/^5/)) {
|
|
776
|
+
authenticating = false
|
|
777
|
+
if (command === 'ehlo') {
|
|
778
|
+
// EHLO command was rejected; fall-back to HELO
|
|
779
|
+
return send_command('HELO', mx.bind_helo)
|
|
780
|
+
}
|
|
781
|
+
if (command === 'rset') {
|
|
782
|
+
// Broken server doesn't accept RSET, terminate the connection
|
|
783
|
+
return client_pool.release_client(socket, mx)
|
|
784
|
+
}
|
|
785
|
+
reason = `${code} ${extc ? `${extc} ` : ''}${response.join(' ')}`
|
|
786
|
+
if (/^rcpt/.test(command) || command === 'dot_lmtp') {
|
|
787
|
+
if (command === 'dot_lmtp') last_recip = ok_recips.shift()
|
|
788
|
+
self.lognotice(`recipient ${last_recip} rejected: ${reason}`)
|
|
789
|
+
last_recip.reason = reason
|
|
790
|
+
|
|
791
|
+
last_recip.dsn_action = 'failed'
|
|
792
|
+
last_recip.dsn_smtp_code = code
|
|
793
|
+
last_recip.dsn_smtp_extc = extc
|
|
794
|
+
last_recip.dsn_status = extc
|
|
795
|
+
last_recip.dsn_smtp_response = response.join(' ')
|
|
796
|
+
last_recip.dsn_remote_mta = mx.exchange
|
|
797
|
+
|
|
798
|
+
bounce_recips.push(last_recip)
|
|
799
|
+
if (command === 'dot_lmtp') {
|
|
800
|
+
response = []
|
|
801
|
+
if (ok_recips.length === 0) {
|
|
802
|
+
return finish_processing_mail(false)
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
for (const rcpt of recipients) {
|
|
807
|
+
rcpt.dsn_action = 'failed'
|
|
808
|
+
rcpt.dsn_smtp_code = code
|
|
809
|
+
rcpt.dsn_smtp_extc = extc
|
|
810
|
+
rcpt.dsn_status = extc
|
|
811
|
+
rcpt.dsn_smtp_response = response.join(' ')
|
|
812
|
+
rcpt.dsn_remote_mta = mx.exchange
|
|
813
|
+
}
|
|
814
|
+
send_command('QUIT')
|
|
815
|
+
processing_mail = false
|
|
816
|
+
return self.bounce(reason, { mx })
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
switch (command) {
|
|
821
|
+
case 'connect':
|
|
822
|
+
send_command('EHLO', mx.bind_helo)
|
|
823
|
+
break
|
|
824
|
+
case 'connect_lmtp':
|
|
825
|
+
send_command('LHLO', mx.bind_helo)
|
|
826
|
+
break
|
|
827
|
+
case 'lhlo':
|
|
828
|
+
case 'ehlo':
|
|
829
|
+
process_ehlo_data()
|
|
830
|
+
break
|
|
831
|
+
case 'starttls': {
|
|
832
|
+
const tls_options = obtls.get_tls_options(mx)
|
|
833
|
+
if (self.force_tls) tls_options.rejectUnauthorized = true
|
|
834
|
+
|
|
835
|
+
smtp_properties = {}
|
|
836
|
+
socket.upgrade(tls_options, (authorized, verifyError, cert, cipher) => {
|
|
837
|
+
const loginfo = {
|
|
838
|
+
verified: authorized,
|
|
839
|
+
}
|
|
840
|
+
if (cipher) {
|
|
841
|
+
loginfo.cipher = cipher.name
|
|
842
|
+
loginfo.version = cipher.version
|
|
843
|
+
}
|
|
844
|
+
if (verifyError) loginfo.error = verifyError
|
|
845
|
+
if (cert?.subject) {
|
|
846
|
+
loginfo.cn = cert.subject.CN
|
|
847
|
+
loginfo.organization = cert.subject.O
|
|
848
|
+
}
|
|
849
|
+
if (cert?.issuer) loginfo.issuer = cert.issuer.O
|
|
850
|
+
if (cert?.valid_to) loginfo.expires = cert.valid_to
|
|
851
|
+
if (cert?.fingerprint) loginfo.fingerprint = cert.fingerprint
|
|
852
|
+
self.loginfo('secured', loginfo)
|
|
853
|
+
|
|
854
|
+
if (self.force_tls && !authorized) {
|
|
855
|
+
processing_mail = false
|
|
856
|
+
socket.end()
|
|
857
|
+
self.temp_fail('Host failed TLS verification required by configuration.')
|
|
858
|
+
client_pool.release_client(socket, mx)
|
|
859
|
+
}
|
|
860
|
+
})
|
|
861
|
+
break
|
|
862
|
+
}
|
|
863
|
+
case 'auth':
|
|
864
|
+
authenticating = false
|
|
865
|
+
authenticated = true
|
|
866
|
+
send_command('MAIL', get_reverse_path_with_params())
|
|
867
|
+
break
|
|
868
|
+
case 'helo':
|
|
869
|
+
send_command('MAIL', get_reverse_path_with_params())
|
|
870
|
+
break
|
|
871
|
+
case 'mail':
|
|
872
|
+
last_recip = recipients[recip_index]
|
|
873
|
+
recip_index++
|
|
874
|
+
send_command('RCPT', `TO:${last_recip.format(!smtp_properties.smtputf8)}`)
|
|
875
|
+
break
|
|
876
|
+
case 'rcpt':
|
|
877
|
+
if (last_recip && code.match(/^250/)) {
|
|
878
|
+
ok_recips.push(last_recip)
|
|
879
|
+
}
|
|
880
|
+
if (recip_index === recipients.length) {
|
|
881
|
+
// End of RCPT TOs
|
|
882
|
+
if (ok_recips.length > 0) {
|
|
883
|
+
send_command('DATA')
|
|
884
|
+
} else {
|
|
885
|
+
finish_processing_mail(false)
|
|
886
|
+
}
|
|
887
|
+
} else {
|
|
888
|
+
last_recip = recipients[recip_index]
|
|
889
|
+
recip_index++
|
|
890
|
+
send_command('RCPT', `TO:${last_recip.format(!smtp_properties.smtputf8)}`)
|
|
891
|
+
}
|
|
892
|
+
break
|
|
893
|
+
case 'data': {
|
|
894
|
+
const data_stream = self.data_stream()
|
|
895
|
+
data_stream.on('data', (data) => {
|
|
896
|
+
self.logdata(`C: ${data}`)
|
|
897
|
+
})
|
|
898
|
+
data_stream.on('error', (err) => {
|
|
899
|
+
self.logerror(`Reading from the data stream failed: ${err}`)
|
|
900
|
+
})
|
|
901
|
+
data_stream.on('end', () => {
|
|
902
|
+
send_command(mx.using_lmtp ? 'dot_lmtp' : 'dot')
|
|
903
|
+
})
|
|
904
|
+
data_stream.pipe(socket, { end: false })
|
|
905
|
+
break
|
|
906
|
+
}
|
|
907
|
+
case 'dot':
|
|
908
|
+
finish_processing_mail(true)
|
|
909
|
+
break
|
|
910
|
+
case 'dot_lmtp':
|
|
911
|
+
if (code.match(/^2/)) lmtp_rcpt_idx++
|
|
912
|
+
if (lmtp_rcpt_idx === ok_recips.length) {
|
|
913
|
+
finish_processing_mail(true)
|
|
914
|
+
}
|
|
915
|
+
break
|
|
916
|
+
case 'quit':
|
|
917
|
+
case 'rset':
|
|
918
|
+
client_pool.release_client(socket, mx)
|
|
919
|
+
break
|
|
920
|
+
default:
|
|
921
|
+
// should never get here - means we did something
|
|
922
|
+
// wrong.
|
|
923
|
+
throw new Error(`Unknown command: ${command}`)
|
|
924
|
+
}
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
if (socket.__fromPool) {
|
|
928
|
+
logger.debug(this, 'got socket, trying to deliver')
|
|
929
|
+
secured = socket.isEncrypted()
|
|
930
|
+
logger.debug(this, `got ${secured ? 'TLS ' : ''}socket, trying to deliver`)
|
|
931
|
+
send_command('MAIL', get_reverse_path_with_params())
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
extend_rcpt_with_dsn(rcpt, dsn) {
|
|
936
|
+
rcpt.dsn_code = dsn.code
|
|
937
|
+
rcpt.dsn_msg = dsn.msg
|
|
938
|
+
rcpt.dsn_status = `${dsn.cls}.${dsn.sub}.${dsn.det}`
|
|
939
|
+
if (dsn.cls == 4) {
|
|
940
|
+
rcpt.dsn_action = 'delayed'
|
|
941
|
+
} else if (dsn.cls == 5) {
|
|
942
|
+
rcpt.dsn_action = 'failed'
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
populate_bounce_message(from, to, reason, cb) {
|
|
947
|
+
let buf = ''
|
|
948
|
+
const original_header_lines = []
|
|
949
|
+
let headers_done = false
|
|
950
|
+
const header = new message.Header()
|
|
951
|
+
|
|
952
|
+
try {
|
|
953
|
+
const data_stream = this.data_stream()
|
|
954
|
+
data_stream.on('data', (data) => {
|
|
955
|
+
if (headers_done === false) {
|
|
956
|
+
buf += data
|
|
957
|
+
let results
|
|
958
|
+
while ((results = utils.line_regexp.exec(buf))) {
|
|
959
|
+
const this_line = results[1]
|
|
960
|
+
if (this_line === '\n' || this_line == '\r\n') {
|
|
961
|
+
headers_done = true
|
|
962
|
+
break
|
|
963
|
+
}
|
|
964
|
+
buf = buf.slice(this_line.length)
|
|
965
|
+
original_header_lines.push(this_line)
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
})
|
|
969
|
+
data_stream.on('end', () => {
|
|
970
|
+
if (original_header_lines.length > 0) {
|
|
971
|
+
header.parse(original_header_lines)
|
|
972
|
+
}
|
|
973
|
+
this.populate_bounce_message_with_headers(from, to, reason, header, cb)
|
|
974
|
+
})
|
|
975
|
+
data_stream.on('error', (err) => {
|
|
976
|
+
cb(err)
|
|
977
|
+
})
|
|
978
|
+
} catch {
|
|
979
|
+
this.populate_bounce_message_with_headers(from, to, reason, header, cb)
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Generates a bounce message
|
|
985
|
+
*
|
|
986
|
+
* hmail.todo.rcpt_to objects should be extended as follows:
|
|
987
|
+
* - dsn_action
|
|
988
|
+
* - dsn_status
|
|
989
|
+
* - dsn_code
|
|
990
|
+
* - dsn_msg
|
|
991
|
+
*
|
|
992
|
+
* - dsn_remote_mta
|
|
993
|
+
*
|
|
994
|
+
* Upstream code/message goes here:
|
|
995
|
+
* - dsn_smtp_code
|
|
996
|
+
* - dsn_smtp_extc
|
|
997
|
+
* - dsn_smtp_response
|
|
998
|
+
*
|
|
999
|
+
* @param from
|
|
1000
|
+
* @param to
|
|
1001
|
+
* @param reason
|
|
1002
|
+
* @param header
|
|
1003
|
+
* @param cb - a callback for fn(err, message_body_lines)
|
|
1004
|
+
*/
|
|
1005
|
+
populate_bounce_message_with_headers(from, to, reason, header, cb) {
|
|
1006
|
+
const CRLF = '\r\n'
|
|
1007
|
+
|
|
1008
|
+
const originalMessageId = header.get('Message-Id')
|
|
1009
|
+
|
|
1010
|
+
const bounce_msg_ = config.get('outbound.bounce_message', 'data')
|
|
1011
|
+
const bounce_msg_html_ = config.get('outbound.bounce_message_html', 'data')
|
|
1012
|
+
const bounce_msg_image_ = config.get('outbound.bounce_message_image', 'data')
|
|
1013
|
+
|
|
1014
|
+
const bounce_header_lines = []
|
|
1015
|
+
const bounce_body_lines = []
|
|
1016
|
+
const bounce_html_lines = []
|
|
1017
|
+
const bounce_image_lines = []
|
|
1018
|
+
let bounce_headers_done = false
|
|
1019
|
+
|
|
1020
|
+
const values = {
|
|
1021
|
+
date: utils.date_to_str(new Date()),
|
|
1022
|
+
me: net_utils.get_primary_host_name(),
|
|
1023
|
+
from,
|
|
1024
|
+
to,
|
|
1025
|
+
subject: header.get_decoded('Subject').trim(),
|
|
1026
|
+
recipients: this.todo.rcpt_to.join(', '),
|
|
1027
|
+
reason,
|
|
1028
|
+
extended_reason: this.todo.rcpt_to
|
|
1029
|
+
.map((recip) => {
|
|
1030
|
+
if (recip.reason) {
|
|
1031
|
+
return `${recip.original}: ${recip.reason}`
|
|
1032
|
+
}
|
|
1033
|
+
})
|
|
1034
|
+
.join('\n'),
|
|
1035
|
+
pid: process.pid,
|
|
1036
|
+
msgid: `<${utils.uuid()}@${net_utils.get_primary_host_name()}>`,
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
for (let line of bounce_msg_) {
|
|
1040
|
+
line = line.replace(/\{(\w+)\}/g, (i, word) => values[word] || '?')
|
|
1041
|
+
|
|
1042
|
+
if (bounce_headers_done == false && line == '') {
|
|
1043
|
+
bounce_headers_done = true
|
|
1044
|
+
} else if (bounce_headers_done == false) {
|
|
1045
|
+
bounce_header_lines.push(line)
|
|
1046
|
+
} else if (bounce_headers_done == true) {
|
|
1047
|
+
bounce_body_lines.push(line)
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const escaped_chars = {
|
|
1052
|
+
'&': 'amp',
|
|
1053
|
+
'<': 'lt',
|
|
1054
|
+
'>': 'gt',
|
|
1055
|
+
'"': 'quot',
|
|
1056
|
+
"'": 'apos',
|
|
1057
|
+
'\r': '#10',
|
|
1058
|
+
'\n': '#13',
|
|
1059
|
+
}
|
|
1060
|
+
const escape_pattern = new RegExp(`[${Object.keys(escaped_chars).join('')}]`, 'g')
|
|
1061
|
+
|
|
1062
|
+
for (let line of bounce_msg_html_) {
|
|
1063
|
+
line = line.replace(/\{(\w+)\}/g, (i, word) => {
|
|
1064
|
+
if (word in values) {
|
|
1065
|
+
return String(values[word]).replace(escape_pattern, (m) => `&${escaped_chars[m]};`)
|
|
1066
|
+
} else {
|
|
1067
|
+
return '?'
|
|
1068
|
+
}
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
bounce_html_lines.push(line)
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
for (const line of bounce_msg_image_) {
|
|
1075
|
+
bounce_image_lines.push(line)
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const boundary = `boundary_${utils.uuid()}`
|
|
1079
|
+
const bounce_body = []
|
|
1080
|
+
|
|
1081
|
+
for (const line of bounce_header_lines) {
|
|
1082
|
+
bounce_body.push(`${line}${CRLF}`)
|
|
1083
|
+
}
|
|
1084
|
+
bounce_body.push(
|
|
1085
|
+
`Content-Type: multipart/report; report-type=delivery-status;${CRLF} boundary="${boundary}"${CRLF}`,
|
|
1086
|
+
)
|
|
1087
|
+
// Adding references to original msg id
|
|
1088
|
+
if (originalMessageId != '') {
|
|
1089
|
+
bounce_body.push(`References: ${originalMessageId.replace(/(\r?\n)*$/, '')}${CRLF}`)
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
bounce_body.push(CRLF)
|
|
1093
|
+
bounce_body.push(`This is a MIME-encapsulated message.${CRLF}`)
|
|
1094
|
+
bounce_body.push(CRLF)
|
|
1095
|
+
|
|
1096
|
+
let boundary_incr = ''
|
|
1097
|
+
if (bounce_html_lines.length > 1) {
|
|
1098
|
+
boundary_incr = 'a'
|
|
1099
|
+
bounce_body.push(`--${boundary}${CRLF}`)
|
|
1100
|
+
bounce_body.push(`Content-Type: multipart/related; boundary="${boundary}${boundary_incr}"${CRLF}`)
|
|
1101
|
+
bounce_body.push(CRLF)
|
|
1102
|
+
bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
|
|
1103
|
+
boundary_incr = 'b'
|
|
1104
|
+
bounce_body.push(`Content-Type: multipart/alternative; boundary="${boundary}${boundary_incr}"${CRLF}`)
|
|
1105
|
+
bounce_body.push(CRLF)
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
|
|
1109
|
+
bounce_body.push(`Content-Type: text/plain; charset=us-ascii${CRLF}`)
|
|
1110
|
+
bounce_body.push(CRLF)
|
|
1111
|
+
for (const line of bounce_body_lines) {
|
|
1112
|
+
bounce_body.push(`${line}${CRLF}`)
|
|
1113
|
+
}
|
|
1114
|
+
bounce_body.push(CRLF)
|
|
1115
|
+
|
|
1116
|
+
if (bounce_html_lines.length > 1) {
|
|
1117
|
+
bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
|
|
1118
|
+
bounce_body.push(`Content-Type: text/html; charset=us-ascii${CRLF}`)
|
|
1119
|
+
bounce_body.push(CRLF)
|
|
1120
|
+
for (const line of bounce_html_lines) {
|
|
1121
|
+
bounce_body.push(`${line}${CRLF}`)
|
|
1122
|
+
}
|
|
1123
|
+
bounce_body.push(CRLF)
|
|
1124
|
+
bounce_body.push(`--${boundary}${boundary_incr}--${CRLF}`)
|
|
1125
|
+
|
|
1126
|
+
if (bounce_image_lines.length > 1) {
|
|
1127
|
+
boundary_incr = 'a'
|
|
1128
|
+
bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
|
|
1129
|
+
//bounce_body.push(`Content-Type: text/html; charset=us-ascii${CRLF}`);
|
|
1130
|
+
//bounce_body.push(CRLF);
|
|
1131
|
+
for (const line of bounce_image_lines) {
|
|
1132
|
+
bounce_body.push(`${line}${CRLF}`)
|
|
1133
|
+
}
|
|
1134
|
+
bounce_body.push(CRLF)
|
|
1135
|
+
bounce_body.push(`--${boundary}${boundary_incr}--${CRLF}`)
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
bounce_body.push(`--${boundary}${CRLF}`)
|
|
1140
|
+
bounce_body.push(`Content-type: message/delivery-status${CRLF}`)
|
|
1141
|
+
bounce_body.push(CRLF)
|
|
1142
|
+
if (originalMessageId != '') {
|
|
1143
|
+
bounce_body.push(`Original-Envelope-Id: ${originalMessageId.replace(/(\r?\n)*$/, '')}${CRLF}`)
|
|
1144
|
+
}
|
|
1145
|
+
bounce_body.push(`Reporting-MTA: dns;${net_utils.get_primary_host_name()}${CRLF}`)
|
|
1146
|
+
if (this.todo.queue_time) {
|
|
1147
|
+
bounce_body.push(`Arrival-Date: ${utils.date_to_str(new Date(this.todo.queue_time))}${CRLF}`)
|
|
1148
|
+
}
|
|
1149
|
+
for (const rcpt_to of this.todo.rcpt_to) {
|
|
1150
|
+
bounce_body.push(CRLF)
|
|
1151
|
+
bounce_body.push(`Final-Recipient: rfc822;${rcpt_to.address}${CRLF}`)
|
|
1152
|
+
let dsn_action = null
|
|
1153
|
+
if (rcpt_to.dsn_action) {
|
|
1154
|
+
dsn_action = rcpt_to.dsn_action
|
|
1155
|
+
} else if (rcpt_to.dsn_code) {
|
|
1156
|
+
if (/^5/.exec(rcpt_to.dsn_code)) {
|
|
1157
|
+
dsn_action = 'failed'
|
|
1158
|
+
} else if (/^4/.exec(rcpt_to.dsn_code)) {
|
|
1159
|
+
dsn_action = 'delayed'
|
|
1160
|
+
} else if (/^2/.exec(rcpt_to.dsn_code)) {
|
|
1161
|
+
dsn_action = 'delivered'
|
|
1162
|
+
}
|
|
1163
|
+
} else if (rcpt_to.dsn_smtp_code) {
|
|
1164
|
+
if (/^5/.exec(rcpt_to.dsn_smtp_code)) {
|
|
1165
|
+
dsn_action = 'failed'
|
|
1166
|
+
} else if (/^4/.exec(rcpt_to.dsn_smtp_code)) {
|
|
1167
|
+
dsn_action = 'delayed'
|
|
1168
|
+
} else if (/^2/.exec(rcpt_to.dsn_smtp_code)) {
|
|
1169
|
+
dsn_action = 'delivered'
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (dsn_action != null) {
|
|
1173
|
+
bounce_body.push(`Action: ${dsn_action}${CRLF}`)
|
|
1174
|
+
}
|
|
1175
|
+
if (rcpt_to.dsn_status) {
|
|
1176
|
+
let { dsn_status } = rcpt_to
|
|
1177
|
+
if (rcpt_to.dsn_code || rcpt_to.dsn_msg) {
|
|
1178
|
+
dsn_status += ' ('
|
|
1179
|
+
if (rcpt_to.dsn_code) {
|
|
1180
|
+
dsn_status += rcpt_to.dsn_code
|
|
1181
|
+
}
|
|
1182
|
+
if (rcpt_to.dsn_code || rcpt_to.dsn_msg) {
|
|
1183
|
+
dsn_status += ' '
|
|
1184
|
+
}
|
|
1185
|
+
if (rcpt_to.dsn_msg) {
|
|
1186
|
+
dsn_status += rcpt_to.dsn_msg
|
|
1187
|
+
}
|
|
1188
|
+
dsn_status += ')'
|
|
1189
|
+
}
|
|
1190
|
+
bounce_body.push(`Status: ${dsn_status}${CRLF}`)
|
|
1191
|
+
}
|
|
1192
|
+
if (rcpt_to.dsn_remote_mta) {
|
|
1193
|
+
bounce_body.push(`Remote-MTA: ${rcpt_to.dsn_remote_mta}${CRLF}`)
|
|
1194
|
+
}
|
|
1195
|
+
let diag_code = null
|
|
1196
|
+
if (rcpt_to.dsn_smtp_code || rcpt_to.dsn_smtp_extc || rcpt_to.dsn_smtp_response) {
|
|
1197
|
+
diag_code = 'smtp;'
|
|
1198
|
+
if (rcpt_to.dsn_smtp_code) {
|
|
1199
|
+
diag_code += `${rcpt_to.dsn_smtp_code} `
|
|
1200
|
+
}
|
|
1201
|
+
if (rcpt_to.dsn_smtp_extc) {
|
|
1202
|
+
diag_code += `${rcpt_to.dsn_smtp_extc} `
|
|
1203
|
+
}
|
|
1204
|
+
if (rcpt_to.dsn_smtp_response) {
|
|
1205
|
+
diag_code += `${rcpt_to.dsn_smtp_response} `
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (diag_code != null) {
|
|
1209
|
+
bounce_body.push(`Diagnostic-Code: ${diag_code}${CRLF}`)
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
bounce_body.push(CRLF)
|
|
1213
|
+
|
|
1214
|
+
bounce_body.push(`--${boundary}${CRLF}`)
|
|
1215
|
+
bounce_body.push(`Content-Description: Undelivered Message Headers${CRLF}`)
|
|
1216
|
+
bounce_body.push(`Content-Type: text/rfc822-headers${CRLF}`)
|
|
1217
|
+
bounce_body.push(CRLF)
|
|
1218
|
+
for (const line of header.header_list) {
|
|
1219
|
+
bounce_body.push(line)
|
|
1220
|
+
}
|
|
1221
|
+
bounce_body.push(CRLF)
|
|
1222
|
+
|
|
1223
|
+
bounce_body.push(`--${boundary}--${CRLF}`)
|
|
1224
|
+
|
|
1225
|
+
cb(null, bounce_body)
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
bounce(err, opts) {
|
|
1229
|
+
this.loginfo(`bouncing mail: ${err}`)
|
|
1230
|
+
if (!this.todo) {
|
|
1231
|
+
this.once('ready', () => {
|
|
1232
|
+
this._bounce(err, opts)
|
|
1233
|
+
})
|
|
1234
|
+
return
|
|
1235
|
+
}
|
|
1236
|
+
this._bounce(err, opts)
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
_bounce(err, opts) {
|
|
1240
|
+
err = new Error(err)
|
|
1241
|
+
if (opts) {
|
|
1242
|
+
err.mx = opts.mx
|
|
1243
|
+
err.deferred_rcpt = opts.fail_recips
|
|
1244
|
+
err.bounced_rcpt = opts.bounce_recips
|
|
1245
|
+
}
|
|
1246
|
+
this.bounce_error = err
|
|
1247
|
+
plugins.run_hooks('bounce', this, err)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
bounce_respond(retval) {
|
|
1251
|
+
if (retval !== constants.cont) {
|
|
1252
|
+
this.loginfo(`Plugin responded with: ${retval}. Not sending bounce.`)
|
|
1253
|
+
return this.discard() // calls next_cb
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const self = this
|
|
1257
|
+
const err = this.bounce_error
|
|
1258
|
+
|
|
1259
|
+
if (!this.todo.mail_from.user) {
|
|
1260
|
+
// double bounce - mail was already a bounce
|
|
1261
|
+
return this.double_bounce('Mail was already a bounce')
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const from = new Address('<>')
|
|
1265
|
+
const recip = new Address(this.todo.mail_from.user, this.todo.mail_from.host)
|
|
1266
|
+
this.populate_bounce_message(from, recip, err, function (err2, data_lines) {
|
|
1267
|
+
if (err2) {
|
|
1268
|
+
return self.double_bounce(`Error populating bounce message: ${err2}`)
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
outbound.send_email(
|
|
1272
|
+
from,
|
|
1273
|
+
recip,
|
|
1274
|
+
data_lines.join(''),
|
|
1275
|
+
(code) => {
|
|
1276
|
+
if (code === constants.deny) {
|
|
1277
|
+
// failed to even queue the mail
|
|
1278
|
+
return self.double_bounce('Unable to queue the bounce message. Not sending bounce!')
|
|
1279
|
+
}
|
|
1280
|
+
self.discard()
|
|
1281
|
+
},
|
|
1282
|
+
{ origin: this },
|
|
1283
|
+
)
|
|
1284
|
+
})
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
double_bounce(err) {
|
|
1288
|
+
this.lognotice(`Double bounce: ${err}`)
|
|
1289
|
+
fs.unlink(this.path).catch(() => {})
|
|
1290
|
+
this.next_cb()
|
|
1291
|
+
// TODO: fill this in... ?
|
|
1292
|
+
// One strategy is perhaps log to an mbox file. What do other servers do?
|
|
1293
|
+
// Another strategy might be delivery "plugins" to cope with this.
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
delivered(ip, port, mode, host, response, ok_recips, fail_recips, bounce_recips, secured, authenticated) {
|
|
1297
|
+
const delay = (Date.now() - this.todo.queue_time) / 1000
|
|
1298
|
+
this.lognotice({
|
|
1299
|
+
'delivered file': this.filename,
|
|
1300
|
+
domain: this.todo.domain,
|
|
1301
|
+
host,
|
|
1302
|
+
ip,
|
|
1303
|
+
port,
|
|
1304
|
+
mode,
|
|
1305
|
+
tls: secured ? 'Y' : 'N',
|
|
1306
|
+
auth: authenticated ? 'Y' : 'N',
|
|
1307
|
+
response,
|
|
1308
|
+
delay,
|
|
1309
|
+
fails: this.num_failures,
|
|
1310
|
+
rcpts: `${ok_recips.length}/${fail_recips.length}/${bounce_recips.length}`,
|
|
1311
|
+
})
|
|
1312
|
+
plugins.run_hooks('delivered', this, [host, ip, response, delay, port, mode, ok_recips, secured, authenticated])
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
discard() {
|
|
1316
|
+
this.refcount--
|
|
1317
|
+
if (this.refcount === 0) {
|
|
1318
|
+
// Remove the file.
|
|
1319
|
+
fs.unlink(this.path).catch(() => {})
|
|
1320
|
+
this.next_cb()
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
convert_temp_failed_to_bounce(err, extra) {
|
|
1325
|
+
for (const rcpt_to of this.todo.rcpt_to) {
|
|
1326
|
+
rcpt_to.dsn_action = 'failed'
|
|
1327
|
+
if (rcpt_to.dsn_status) {
|
|
1328
|
+
rcpt_to.dsn_status = `${rcpt_to.dsn_status}`.replace(/^4/, '5')
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return this.bounce(err, extra)
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
temp_fail(err, extra) {
|
|
1335
|
+
logger.debug(this, `Temp fail for: ${err}`)
|
|
1336
|
+
this.num_failures++
|
|
1337
|
+
|
|
1338
|
+
// Test for max failures which is configurable.
|
|
1339
|
+
if (this.num_failures > obc.cfg.temp_fail_intervals.length) {
|
|
1340
|
+
return this.convert_temp_failed_to_bounce(`Too many failures (${err})`, extra)
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const delay = obc.cfg.temp_fail_intervals[this.num_failures - 1]
|
|
1344
|
+
|
|
1345
|
+
plugins.run_hooks('deferred', this, { delay, err, ...(extra || {}) })
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
async deferred_respond(retval, msg, params) {
|
|
1349
|
+
if (retval !== constants.cont && retval !== constants.denysoft) {
|
|
1350
|
+
this.loginfo(`plugin responded with: ${retval}. Not deferring. Deleting mail.`)
|
|
1351
|
+
return this.discard() // calls next_cb
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
let delay = params.delay * 1000
|
|
1355
|
+
|
|
1356
|
+
if (retval === constants.denysoft) {
|
|
1357
|
+
delay = parseInt(msg, 10) * 1000
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
this.loginfo(`Temp failing ${this.filename} for ${delay / 1000} seconds: ${params.err}`)
|
|
1361
|
+
const parts = _qfile.parts(this.filename)
|
|
1362
|
+
parts.next_attempt = Date.now() + delay
|
|
1363
|
+
parts.attempts = this.num_failures
|
|
1364
|
+
const new_filename = _qfile.name(parts)
|
|
1365
|
+
|
|
1366
|
+
try {
|
|
1367
|
+
await fs.rename(this.path, path.join(queue_dir, new_filename))
|
|
1368
|
+
this.path = path.join(queue_dir, new_filename)
|
|
1369
|
+
this.filename = new_filename
|
|
1370
|
+
|
|
1371
|
+
this.next_cb()
|
|
1372
|
+
|
|
1373
|
+
temp_fail_queue.add(this.filename, delay, () => {
|
|
1374
|
+
delivery_queue.push(this)
|
|
1375
|
+
})
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
return this.bounce(`Error re-queueing email: ${err}`)
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// The following handler impacts outgoing mail. It removes the queue file.
|
|
1382
|
+
delivered_respond(retval, msg) {
|
|
1383
|
+
if (retval !== constants.cont && retval !== constants.ok) {
|
|
1384
|
+
this.logwarn('delivered plugin responded', { retval, msg })
|
|
1385
|
+
}
|
|
1386
|
+
this.discard()
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
get_force_tls(mx) {
|
|
1390
|
+
if (!mx.exchange) return false
|
|
1391
|
+
if (!obtls.cfg.force_tls_hosts) return false
|
|
1392
|
+
|
|
1393
|
+
if (net_utils.ip_in_list(obtls.cfg.force_tls_hosts, mx.exchange)) {
|
|
1394
|
+
this.logdebug(`Forcing TLS for host ${mx.exchange}`)
|
|
1395
|
+
return true
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (mx.from_dns) {
|
|
1399
|
+
// the MX was looked up in DNS and already resolved to IP(s).
|
|
1400
|
+
// This checks the hostname.
|
|
1401
|
+
if (net_utils.ip_in_list(obtls.cfg.force_tls_hosts, mx.from_dns)) {
|
|
1402
|
+
this.logdebug(`Forcing TLS for host ${mx.from_dns}`)
|
|
1403
|
+
return true
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if (net_utils.ip_in_list(obtls.cfg.force_tls_hosts, this.todo.domain)) {
|
|
1408
|
+
this.logdebug(`Forcing TLS for domain ${this.todo.domain}`)
|
|
1409
|
+
return true
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
return false
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
sort_mx(mx_list) {
|
|
1416
|
+
// MXs must be sorted by priority.
|
|
1417
|
+
const sorted = mx_list.sort((a, b) => a.priority - b.priority)
|
|
1418
|
+
|
|
1419
|
+
// Matched priorities must be randomly shuffled.
|
|
1420
|
+
// This isn't a very good shuffle but it'll do for now.
|
|
1421
|
+
for (let i = 0, l = sorted.length - 1; i < l; i++) {
|
|
1422
|
+
if (sorted[i].priority === sorted[i + 1].priority) {
|
|
1423
|
+
if (Math.round(Math.random())) {
|
|
1424
|
+
// 0 or 1
|
|
1425
|
+
const j = sorted[i]
|
|
1426
|
+
sorted[i] = sorted[i + 1]
|
|
1427
|
+
sorted[i + 1] = j
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
return sorted
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
split_to_new_recipients(recipients, response, cb) {
|
|
1436
|
+
const hmail = this
|
|
1437
|
+
if (recipients.length === hmail.todo.rcpt_to.length) {
|
|
1438
|
+
// Split to new for no reason - increase refcount and return self
|
|
1439
|
+
hmail.refcount++
|
|
1440
|
+
return cb(hmail)
|
|
1441
|
+
}
|
|
1442
|
+
const fname = _qfile.name()
|
|
1443
|
+
const tmp_path = path.join(queue_dir, `${_qfile.platformDOT}${fname}`)
|
|
1444
|
+
const ws = new FsyncWriteStream(tmp_path, {
|
|
1445
|
+
flags: constants.WRITE_EXCL,
|
|
1446
|
+
})
|
|
1447
|
+
function err_handler(err, location) {
|
|
1448
|
+
logger.error(this, `Error while splitting to new recipients (${location}): ${err}`)
|
|
1449
|
+
for (const rcpt of hmail.todo.rcpt_to) {
|
|
1450
|
+
hmail.extend_rcpt_with_dsn(rcpt, DSN.sys_unspecified(`Error splitting to new recipients: ${err}`))
|
|
1451
|
+
}
|
|
1452
|
+
hmail.bounce(`Error splitting to new recipients: ${err}`)
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
ws.on('error', (err) => {
|
|
1456
|
+
err_handler(err, 'tmp file writer')
|
|
1457
|
+
})
|
|
1458
|
+
|
|
1459
|
+
let writing = false
|
|
1460
|
+
|
|
1461
|
+
function write_more() {
|
|
1462
|
+
if (writing) return
|
|
1463
|
+
writing = true
|
|
1464
|
+
const rs = hmail.data_stream()
|
|
1465
|
+
rs.pipe(ws, { end: false })
|
|
1466
|
+
rs.on('error', (err) => {
|
|
1467
|
+
err_handler(err, 'hmail.data_stream reader')
|
|
1468
|
+
})
|
|
1469
|
+
rs.on('end', async () => {
|
|
1470
|
+
try {
|
|
1471
|
+
await ws.close()
|
|
1472
|
+
const dest_path = path.join(queue_dir, fname)
|
|
1473
|
+
await fs.rename(tmp_path, dest_path)
|
|
1474
|
+
const split_mail = new HMailItem(fname, dest_path, hmail.notes)
|
|
1475
|
+
split_mail.once('ready', () => {
|
|
1476
|
+
cb(split_mail)
|
|
1477
|
+
})
|
|
1478
|
+
} catch (err) {
|
|
1479
|
+
err_handler(err, 'tmp file close/rename')
|
|
1480
|
+
}
|
|
1481
|
+
})
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
ws.on('error', (err) => {
|
|
1485
|
+
logger.error(this, `Unable to write queue file (${fname}): ${err}`)
|
|
1486
|
+
ws.destroy()
|
|
1487
|
+
for (const rcpt of hmail.todo.rcpt_to) {
|
|
1488
|
+
hmail.extend_rcpt_with_dsn(rcpt, DSN.sys_unspecified(`Error re-queueing some recipients: ${err}`))
|
|
1489
|
+
}
|
|
1490
|
+
hmail.bounce(`Error re-queueing some recipients: ${err}`)
|
|
1491
|
+
})
|
|
1492
|
+
|
|
1493
|
+
const new_todo = JSON.parse(JSON.stringify(hmail.todo))
|
|
1494
|
+
new_todo.rcpt_to = recipients
|
|
1495
|
+
outbound.build_todo(new_todo, ws, write_more)
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
module.exports = HMailItem
|
|
1500
|
+
module.exports.obtls = obtls
|
|
1501
|
+
|
|
1502
|
+
logger.add_log_methods(HMailItem)
|
|
1503
|
+
|
|
1504
|
+
const smtp_regexp = /^([2345]\d\d)([ -])#?(?:(\d\.\d\.\d)\s)?(.*)/
|