haraka 0.0.32 → 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 +1872 -62
- package/CLAUDE.md +40 -0
- package/CONTRIBUTORS.md +34 -0
- package/Dockerfile +50 -0
- package/GEMINI.md +38 -0
- package/LICENSE +2 -1
- package/Plugins.md +227 -0
- package/README.md +100 -115
- 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 +91 -29
- 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
- package/lib/index.js +0 -371
package/connection.js
ADDED
|
@@ -0,0 +1,1863 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// a single connection
|
|
3
|
+
|
|
4
|
+
const dns = require('node:dns')
|
|
5
|
+
const os = require('node:os')
|
|
6
|
+
|
|
7
|
+
// npm libs
|
|
8
|
+
const ipaddr = require('ipaddr.js')
|
|
9
|
+
const config = require('haraka-config')
|
|
10
|
+
const constants = require('haraka-constants')
|
|
11
|
+
const net_utils = require('haraka-net-utils')
|
|
12
|
+
const Notes = require('haraka-notes')
|
|
13
|
+
const utils = require('haraka-utils')
|
|
14
|
+
const { Address } = require('./address')
|
|
15
|
+
const ResultStore = require('haraka-results')
|
|
16
|
+
|
|
17
|
+
// Haraka libs
|
|
18
|
+
const logger = require('./logger')
|
|
19
|
+
const trans = require('./transaction')
|
|
20
|
+
const plugins = require('./plugins')
|
|
21
|
+
const rfc1869 = utils.rfc1869
|
|
22
|
+
const outbound = require('./outbound')
|
|
23
|
+
|
|
24
|
+
const states = constants.connection.state
|
|
25
|
+
|
|
26
|
+
const cfg = config.get('connection.ini', {
|
|
27
|
+
booleans: [
|
|
28
|
+
'-main.strict_rfc1869',
|
|
29
|
+
'-main.postel',
|
|
30
|
+
'+main.smtputf8',
|
|
31
|
+
'+headers.add_received',
|
|
32
|
+
'+headers.show_version',
|
|
33
|
+
'+headers.clean_auth_results',
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
class Connection {
|
|
38
|
+
constructor(client, server) {
|
|
39
|
+
this.client = client
|
|
40
|
+
this.server = server
|
|
41
|
+
|
|
42
|
+
this.local = {
|
|
43
|
+
ip: null,
|
|
44
|
+
port: null,
|
|
45
|
+
host: net_utils.get_primary_host_name(),
|
|
46
|
+
info: 'Haraka',
|
|
47
|
+
}
|
|
48
|
+
this.remote = {
|
|
49
|
+
ip: null,
|
|
50
|
+
port: null,
|
|
51
|
+
host: null,
|
|
52
|
+
info: null,
|
|
53
|
+
closed: false,
|
|
54
|
+
is_private: false,
|
|
55
|
+
is_local: false,
|
|
56
|
+
}
|
|
57
|
+
this.hello = {
|
|
58
|
+
host: null,
|
|
59
|
+
verb: null,
|
|
60
|
+
}
|
|
61
|
+
this.tls = {
|
|
62
|
+
enabled: false,
|
|
63
|
+
advertised: false,
|
|
64
|
+
verified: false,
|
|
65
|
+
cipher: {},
|
|
66
|
+
}
|
|
67
|
+
this.proxy = {
|
|
68
|
+
allowed: false,
|
|
69
|
+
ip: null,
|
|
70
|
+
type: null,
|
|
71
|
+
timer: null,
|
|
72
|
+
}
|
|
73
|
+
this.set('tls', 'enabled', !!server.has_tls)
|
|
74
|
+
|
|
75
|
+
this.current_data = null
|
|
76
|
+
this.current_line = null
|
|
77
|
+
this.state = states.PAUSE
|
|
78
|
+
this.encoding = 'utf8'
|
|
79
|
+
this.prev_state = null
|
|
80
|
+
this.loop_code = null
|
|
81
|
+
this.loop_msg = null
|
|
82
|
+
this.uuid = utils.uuid()
|
|
83
|
+
this.notes = new Notes()
|
|
84
|
+
this.transaction = null
|
|
85
|
+
this.tran_count = 0
|
|
86
|
+
this.capabilities = null
|
|
87
|
+
this.early_talker = false
|
|
88
|
+
this.pipelining = false
|
|
89
|
+
this._relaying = false
|
|
90
|
+
this.esmtp = false
|
|
91
|
+
this.last_response = null
|
|
92
|
+
this.hooks_to_run = []
|
|
93
|
+
this.start_time = Date.now()
|
|
94
|
+
this.last_reject = ''
|
|
95
|
+
this.totalbytes = 0
|
|
96
|
+
this.rcpt_count = {
|
|
97
|
+
accept: 0,
|
|
98
|
+
tempfail: 0,
|
|
99
|
+
reject: 0,
|
|
100
|
+
}
|
|
101
|
+
this.msg_count = {
|
|
102
|
+
accept: 0,
|
|
103
|
+
tempfail: 0,
|
|
104
|
+
reject: 0,
|
|
105
|
+
}
|
|
106
|
+
this.results = new ResultStore(this)
|
|
107
|
+
this.errors = 0
|
|
108
|
+
this.last_rcpt_msg = null
|
|
109
|
+
this.hook = null
|
|
110
|
+
if (cfg.headers.show_version) {
|
|
111
|
+
this.local.info += `/${utils.getVersion(__dirname)}`
|
|
112
|
+
}
|
|
113
|
+
Connection.setupClient(this)
|
|
114
|
+
}
|
|
115
|
+
static setupClient(self) {
|
|
116
|
+
const ip = self.client.remoteAddress
|
|
117
|
+
if (!ip) {
|
|
118
|
+
self.logdebug('setupClient got no IP address for this connection!')
|
|
119
|
+
self.client.destroy()
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const local_addr = self.server.address()
|
|
124
|
+
self.set('local', 'ip', ipaddr.process(self.client.localAddress || local_addr.address).toString())
|
|
125
|
+
self.set('local', 'port', self.client.localPort || local_addr.port)
|
|
126
|
+
self.results.add({ name: 'local' }, self.local)
|
|
127
|
+
|
|
128
|
+
self.set('remote', 'ip', ipaddr.process(ip).toString())
|
|
129
|
+
self.set('remote', 'port', self.client.remotePort)
|
|
130
|
+
self.results.add({ name: 'remote' }, self.remote)
|
|
131
|
+
|
|
132
|
+
self.lognotice('connect', {
|
|
133
|
+
ip: self.remote.ip,
|
|
134
|
+
port: self.remote.port,
|
|
135
|
+
local_ip: self.local.ip,
|
|
136
|
+
local_port: self.local.port,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
if (!self.client.on) return
|
|
140
|
+
|
|
141
|
+
const log_data = { ip: self.remote.ip }
|
|
142
|
+
if (self.remote.host) log_data.host = self.remote.host
|
|
143
|
+
|
|
144
|
+
self.client.on('end', () => {
|
|
145
|
+
if (self.state >= states.DISCONNECTING) return
|
|
146
|
+
self.remote.closed = true
|
|
147
|
+
self.loginfo('client half closed connection', log_data)
|
|
148
|
+
self.fail()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
self.client.on('close', () => {
|
|
152
|
+
if (self.state >= states.DISCONNECTING) return
|
|
153
|
+
self.remote.closed = true
|
|
154
|
+
self.loginfo('client dropped connection', log_data)
|
|
155
|
+
self.fail()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
self.client.on('error', (err) => {
|
|
159
|
+
if (self.state >= states.DISCONNECTING) return
|
|
160
|
+
self.loginfo(`client connection error: ${err}`, log_data)
|
|
161
|
+
self.fail()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
self.client.on('timeout', () => {
|
|
165
|
+
// FIN has sent, when timeout just destroy socket
|
|
166
|
+
if (self.state >= states.DISCONNECTED) {
|
|
167
|
+
self.client.destroy()
|
|
168
|
+
self.loginfo(`timeout, destroy socket (state:${self.state})`)
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
if (self.state >= states.DISCONNECTING) return
|
|
172
|
+
self.respond(421, 'timeout', () => {
|
|
173
|
+
self.fail('client connection timed out', log_data)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
self.client.on('data', (data) => {
|
|
178
|
+
self.process_data(data)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// SMTPS pre-parser state: proxy means the PROXY line was already consumed;
|
|
182
|
+
// peer_allowed means a trusted PROXY peer sent direct TLS instead.
|
|
183
|
+
const smtps = self.client.haraka_smtps
|
|
184
|
+
if (smtps?.proxy) {
|
|
185
|
+
self.proxy.allowed = true
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (smtps?.peer_allowed) {
|
|
190
|
+
plugins.run_hooks('connect_init', self)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (net_utils.is_haproxy_allowed(self.remote.ip)) {
|
|
195
|
+
self.proxy.allowed = true
|
|
196
|
+
// Wait for PROXY command
|
|
197
|
+
self.proxy.timer = setTimeout(() => {
|
|
198
|
+
self.respond(421, 'PROXY timeout', () => {
|
|
199
|
+
self.disconnect()
|
|
200
|
+
})
|
|
201
|
+
}, 30 * 1000)
|
|
202
|
+
} else {
|
|
203
|
+
plugins.run_hooks('connect_init', self)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
setTLS(obj) {
|
|
207
|
+
this.set('hello', 'host', undefined)
|
|
208
|
+
this.set('tls', 'enabled', true)
|
|
209
|
+
for (const t of ['cipher', 'verified', 'verifyError', 'peerCertificate']) {
|
|
210
|
+
if (obj[t] === undefined) continue
|
|
211
|
+
this.set('tls', t, obj[t])
|
|
212
|
+
}
|
|
213
|
+
// prior to 2017-07, authorized and verified were both used. Verified
|
|
214
|
+
// seems to be the more common and has the property updated in the
|
|
215
|
+
// tls object. However, authorized has been up-to-date in the notes. Store
|
|
216
|
+
// in both, for backwards compatibility.
|
|
217
|
+
this.notes.tls = {
|
|
218
|
+
authorized: obj.verified, // legacy name
|
|
219
|
+
authorizationError: obj.verifyError,
|
|
220
|
+
cipher: obj.cipher,
|
|
221
|
+
peerCertificate: obj.peerCertificate,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
set(prop_str, val) {
|
|
225
|
+
if (arguments.length === 3) {
|
|
226
|
+
prop_str = `${arguments[0]}.${arguments[1]}`
|
|
227
|
+
val = arguments[2]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const path_parts = prop_str.split('.')
|
|
231
|
+
let loc = this
|
|
232
|
+
for (let i = 0; i < path_parts.length; i++) {
|
|
233
|
+
const part = path_parts[i]
|
|
234
|
+
if (part === '__proto__' || part === 'constructor') continue
|
|
235
|
+
|
|
236
|
+
// while another part remains
|
|
237
|
+
if (i < path_parts.length - 1) {
|
|
238
|
+
if (loc[part] === undefined) loc[part] = {} // initialize
|
|
239
|
+
loc = loc[part] // descend
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// last part, so assign the value
|
|
244
|
+
loc[part] = val
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Set is_private, is_local automatically when remote.ip is set
|
|
248
|
+
if (prop_str === 'remote.ip') {
|
|
249
|
+
this.set('remote.is_local', net_utils.is_local_ip(this.remote.ip))
|
|
250
|
+
if (this.remote.is_local) {
|
|
251
|
+
this.set('remote.is_private', true)
|
|
252
|
+
} else {
|
|
253
|
+
this.set('remote.is_private', net_utils.is_private_ip(this.remote.ip))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
get(prop_str) {
|
|
258
|
+
return prop_str.split('.').reduce((prev, curr) => {
|
|
259
|
+
return prev ? prev[curr] : undefined
|
|
260
|
+
}, this)
|
|
261
|
+
}
|
|
262
|
+
set relaying(val) {
|
|
263
|
+
if (this.transaction) {
|
|
264
|
+
this.transaction._relaying = val
|
|
265
|
+
} else {
|
|
266
|
+
this._relaying = val
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
get relaying() {
|
|
270
|
+
if (this.transaction && '_relaying' in this.transaction) return this.transaction._relaying
|
|
271
|
+
return this._relaying
|
|
272
|
+
}
|
|
273
|
+
process_line(line) {
|
|
274
|
+
if (this.state >= states.DISCONNECTING) {
|
|
275
|
+
if (logger.would_log(logger.LOGPROTOCOL)) {
|
|
276
|
+
this.logprotocol(`C: (after-disconnect): ${this.current_line}`, {
|
|
277
|
+
state: this.state,
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
this.loginfo(`data after disconnect from ${this.remote.ip}`)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (this.state === states.DATA) {
|
|
285
|
+
if (logger.would_log(logger.LOGDATA)) {
|
|
286
|
+
this.logdata(`C: ${line}`)
|
|
287
|
+
}
|
|
288
|
+
this.accumulate_data(line)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
this.current_line = line.toString(this.encoding).replace(/\r?\n/, '')
|
|
293
|
+
if (logger.would_log(logger.LOGPROTOCOL)) {
|
|
294
|
+
this.logprotocol(`C: ${this.current_line}`, { state: this.state })
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check for non-ASCII characters
|
|
298
|
+
/* eslint no-control-regex: 0 */
|
|
299
|
+
if (/[^\x00-\x7F]/.test(this.current_line)) {
|
|
300
|
+
// See if this is a TLS handshake
|
|
301
|
+
const buf = Buffer.from(this.current_line.slice(0, 3), 'binary')
|
|
302
|
+
if (
|
|
303
|
+
buf[0] === 0x16 &&
|
|
304
|
+
buf[1] === 0x03 &&
|
|
305
|
+
(buf[2] === 0x00 || buf[2] === 0x01) // SSLv3/TLS1.x format
|
|
306
|
+
) {
|
|
307
|
+
// Nuke the current input buffer to prevent processing further input
|
|
308
|
+
this.current_data = null
|
|
309
|
+
this.respond(501, 'SSL attempted over a non-SSL socket')
|
|
310
|
+
this.disconnect()
|
|
311
|
+
return
|
|
312
|
+
} else if (this.hello.verb == 'HELO') {
|
|
313
|
+
return this.respond(501, 'Syntax error (8-bit characters not allowed)')
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (this.state === states.CMD) {
|
|
318
|
+
this.state = states.PAUSE_SMTP
|
|
319
|
+
const matches = /^([^ ]*)( +(.*))?$/.exec(this.current_line)
|
|
320
|
+
if (!matches) {
|
|
321
|
+
return plugins.run_hooks('unrecognized_command', this, [this.current_line])
|
|
322
|
+
}
|
|
323
|
+
const cmd = matches[1]
|
|
324
|
+
const method = `cmd_${cmd.toLowerCase()}`
|
|
325
|
+
const remaining = matches[3] || ''
|
|
326
|
+
if (this[method]) {
|
|
327
|
+
try {
|
|
328
|
+
this[method](remaining)
|
|
329
|
+
} catch (err) {
|
|
330
|
+
if (err.stack) {
|
|
331
|
+
this.logerror(`${method} failed: ${err}`)
|
|
332
|
+
for (const line of err.stack.split('\n')) this.logerror(line)
|
|
333
|
+
} else {
|
|
334
|
+
this.logerror(`${method} failed: ${err}`)
|
|
335
|
+
}
|
|
336
|
+
this.respond(421, 'Internal Server Error', () => {
|
|
337
|
+
this.disconnect()
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
// unrecognized command
|
|
342
|
+
plugins.run_hooks('unrecognized_command', this, [cmd, remaining])
|
|
343
|
+
}
|
|
344
|
+
} else if (this.state === states.LOOP) {
|
|
345
|
+
// Allow QUIT
|
|
346
|
+
if (this.current_line.toUpperCase() === 'QUIT') {
|
|
347
|
+
this.state = states.PAUSE_SMTP
|
|
348
|
+
this.cmd_quit()
|
|
349
|
+
} else {
|
|
350
|
+
this.respond(this.loop_code, this.loop_msg)
|
|
351
|
+
}
|
|
352
|
+
} else if (this.state === states.PAUSE_SMTP) {
|
|
353
|
+
// Do nothing
|
|
354
|
+
} else {
|
|
355
|
+
throw new Error(`unknown state ${this.state}`)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
process_data(data) {
|
|
359
|
+
if (this.state >= states.DISCONNECTING) {
|
|
360
|
+
this.loginfo(`data after disconnect from ${this.remote.ip}`)
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!this.current_data || !this.current_data.length) {
|
|
365
|
+
this.current_data = data
|
|
366
|
+
} else {
|
|
367
|
+
// Data left over in buffer
|
|
368
|
+
const buf = Buffer.concat([this.current_data, data], this.current_data.length + data.length)
|
|
369
|
+
this.current_data = buf
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
this._process_data()
|
|
373
|
+
}
|
|
374
|
+
_process_data() {
|
|
375
|
+
// We *must* detect disconnected connections here as the state
|
|
376
|
+
// only transitions to states.CMD in the respond function below.
|
|
377
|
+
// Otherwise if multiple commands are pipelined and then the
|
|
378
|
+
// connection is dropped; we'll end up in the function forever.
|
|
379
|
+
if (this.state >= states.DISCONNECTING) return
|
|
380
|
+
|
|
381
|
+
let maxlength
|
|
382
|
+
if (this.state === states.PAUSE_DATA || this.state === states.DATA) {
|
|
383
|
+
maxlength = cfg.max.data_line_length
|
|
384
|
+
} else {
|
|
385
|
+
maxlength = cfg.max.line_length
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let offset
|
|
389
|
+
while (this.current_data && (offset = utils.indexOfLF(this.current_data, maxlength)) !== -1) {
|
|
390
|
+
if (this.state === states.PAUSE_DATA) {
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
let this_line = this.current_data.slice(0, offset + 1)
|
|
394
|
+
// Hack: bypass this code to allow HAProxy's PROXY extension
|
|
395
|
+
const proxyStart = this.proxy.allowed && /^PROXY /.test(this_line)
|
|
396
|
+
if (this.state === states.PAUSE && proxyStart) {
|
|
397
|
+
if (this.proxy.timer) clearTimeout(this.proxy.timer)
|
|
398
|
+
this.state = states.CMD
|
|
399
|
+
this.current_data = this.current_data.slice(this_line.length)
|
|
400
|
+
this.process_line(this_line)
|
|
401
|
+
}
|
|
402
|
+
// Detect early_talker but allow PIPELINING extension (ESMTP)
|
|
403
|
+
else if ((this.state === states.PAUSE || this.state === states.PAUSE_SMTP) && !this.esmtp) {
|
|
404
|
+
// Allow EHLO/HELO to be pipelined with PROXY
|
|
405
|
+
if (this.proxy.allowed && /^(?:EH|HE)LO /i.test(this_line)) return
|
|
406
|
+
if (!this.early_talker) {
|
|
407
|
+
this_line = this_line.toString().replace(/\r?\n/, '')
|
|
408
|
+
this.logdebug('[early_talker]', {
|
|
409
|
+
state: this.state,
|
|
410
|
+
esmtp: this.esmtp,
|
|
411
|
+
line: this_line,
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
this.early_talker = true
|
|
415
|
+
setImmediate(() => {
|
|
416
|
+
this._process_data()
|
|
417
|
+
})
|
|
418
|
+
break
|
|
419
|
+
} else if ((this.state === states.PAUSE || this.state === states.PAUSE_SMTP) && this.esmtp) {
|
|
420
|
+
let valid = true
|
|
421
|
+
const cmd = this_line.toString('ascii').slice(0, 4).toUpperCase()
|
|
422
|
+
switch (cmd) {
|
|
423
|
+
case 'RSET':
|
|
424
|
+
case 'MAIL':
|
|
425
|
+
case 'SEND':
|
|
426
|
+
case 'SOML':
|
|
427
|
+
case 'SAML':
|
|
428
|
+
case 'RCPT':
|
|
429
|
+
// These can be anywhere in the group
|
|
430
|
+
break
|
|
431
|
+
default:
|
|
432
|
+
// Anything else *MUST* be the last command in the group
|
|
433
|
+
if (this_line.length !== this.current_data.length) {
|
|
434
|
+
valid = false
|
|
435
|
+
}
|
|
436
|
+
break
|
|
437
|
+
}
|
|
438
|
+
if (valid) {
|
|
439
|
+
// Valid PIPELINING
|
|
440
|
+
// We *don't want to process this yet otherwise the
|
|
441
|
+
// current_data buffer will be lost. The respond()
|
|
442
|
+
// function will call this function again once it
|
|
443
|
+
// has reset the state back to states.CMD and this
|
|
444
|
+
// ensures that we only process one command at a
|
|
445
|
+
// time.
|
|
446
|
+
this.pipelining = true
|
|
447
|
+
this.logdebug(`pipeline: ${this_line}`)
|
|
448
|
+
} else {
|
|
449
|
+
// Invalid pipeline sequence
|
|
450
|
+
// Treat this as early talker
|
|
451
|
+
if (!this.early_talker) {
|
|
452
|
+
this.logdebug('[early_talker]', {
|
|
453
|
+
state: this.state,
|
|
454
|
+
esmtp: this.esmtp,
|
|
455
|
+
line: this_line,
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
this.early_talker = true
|
|
459
|
+
setImmediate(() => {
|
|
460
|
+
this._process_data()
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
break
|
|
464
|
+
} else {
|
|
465
|
+
this.current_data = this.current_data.slice(this_line.length)
|
|
466
|
+
this.process_line(this_line)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (
|
|
471
|
+
this.current_data &&
|
|
472
|
+
this.current_data.length > maxlength &&
|
|
473
|
+
utils.indexOfLF(this.current_data, maxlength) === -1
|
|
474
|
+
) {
|
|
475
|
+
if (this.state !== states.DATA && this.state !== states.PAUSE_DATA) {
|
|
476
|
+
// In command mode, reject:
|
|
477
|
+
this.client.pause()
|
|
478
|
+
this.current_data = null
|
|
479
|
+
return this.respond(521, 'Command line too long', () => {
|
|
480
|
+
this.disconnect()
|
|
481
|
+
})
|
|
482
|
+
} else {
|
|
483
|
+
this.loginfo(`DATA line length (${this.current_data.length}) exceeds limit of ${maxlength} bytes`)
|
|
484
|
+
this.transaction.notes.data_line_length_exceeded = true
|
|
485
|
+
const b = Buffer.concat(
|
|
486
|
+
[
|
|
487
|
+
this.current_data.slice(0, maxlength - 2),
|
|
488
|
+
Buffer.from('\r\n ', 'utf8'),
|
|
489
|
+
this.current_data.slice(maxlength - 2),
|
|
490
|
+
],
|
|
491
|
+
this.current_data.length + 3,
|
|
492
|
+
)
|
|
493
|
+
this.current_data = b
|
|
494
|
+
return this._process_data()
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
respond(code, msg, func) {
|
|
499
|
+
let uuid = ''
|
|
500
|
+
let messages
|
|
501
|
+
|
|
502
|
+
if (this.state === states.DISCONNECTED) {
|
|
503
|
+
if (func) func()
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
// Check to see if DSN object was passed in
|
|
507
|
+
if (typeof msg === 'object' && msg.constructor.name === 'DSN') {
|
|
508
|
+
// Override
|
|
509
|
+
code = msg.code
|
|
510
|
+
msg = msg.reply
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (!Array.isArray(msg)) {
|
|
514
|
+
messages = msg.toString().split(/\n/)
|
|
515
|
+
} else {
|
|
516
|
+
messages = msg.slice()
|
|
517
|
+
}
|
|
518
|
+
messages = messages.filter((msg2) => /\S/.test(msg2))
|
|
519
|
+
|
|
520
|
+
// Multiline AUTH PLAIN as in RFC-4954 page 8.
|
|
521
|
+
if (code === 334 && !messages.length) {
|
|
522
|
+
messages = [' ']
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (code >= 400) {
|
|
526
|
+
this.last_reject = `${code} ${messages.join(' ')}`
|
|
527
|
+
if (cfg.uuid.deny_chars) {
|
|
528
|
+
uuid = (this.transaction || this).uuid
|
|
529
|
+
if (cfg.uuid.deny_chars > 1) {
|
|
530
|
+
uuid = uuid.slice(0, cfg.uuid.deny_chars)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
let mess
|
|
536
|
+
let buf = ''
|
|
537
|
+
const hostname = os.hostname().split('.').shift()
|
|
538
|
+
const _uuid = uuid ? `[${uuid}@${hostname}] ` : ''
|
|
539
|
+
|
|
540
|
+
while ((mess = messages.shift())) {
|
|
541
|
+
const line = `${code}${messages.length ? '-' : ' '}${_uuid}${mess}`
|
|
542
|
+
this.logprotocol(`S: ${line}`)
|
|
543
|
+
buf = `${buf}${line}\r\n`
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (this.client.write === undefined) return buf // testing
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
this.client.write(buf)
|
|
550
|
+
} catch (err) {
|
|
551
|
+
return this.fail(`Writing response: ${buf} failed: ${err}`)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Store the last response
|
|
555
|
+
this.last_response = buf
|
|
556
|
+
|
|
557
|
+
// Don't change loop state
|
|
558
|
+
if (this.state !== states.LOOP) {
|
|
559
|
+
this.state = states.CMD
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Run optional closure before handling and further commands
|
|
563
|
+
if (func) func()
|
|
564
|
+
|
|
565
|
+
// Process any buffered commands (PIPELINING)
|
|
566
|
+
this._process_data()
|
|
567
|
+
}
|
|
568
|
+
fail(err, err_data) {
|
|
569
|
+
if (err) this.logwarn(err, err_data)
|
|
570
|
+
this.hooks_to_run = []
|
|
571
|
+
this.disconnect()
|
|
572
|
+
}
|
|
573
|
+
disconnect() {
|
|
574
|
+
if (this.state >= states.DISCONNECTING) return
|
|
575
|
+
this.state = states.DISCONNECTING
|
|
576
|
+
this.current_data = null // don't process any more data we have already received
|
|
577
|
+
this.reset_transaction(() => {
|
|
578
|
+
plugins.run_hooks('disconnect', this)
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
disconnect_respond() {
|
|
582
|
+
const logdetail = {
|
|
583
|
+
ip: this.remote.ip,
|
|
584
|
+
rdns: this.remote.host ? this.remote.host : '',
|
|
585
|
+
helo: this.hello.host ? this.hello.host : '',
|
|
586
|
+
relay: this.relaying ? 'Y' : 'N',
|
|
587
|
+
early: this.early_talker ? 'Y' : 'N',
|
|
588
|
+
esmtp: this.esmtp ? 'Y' : 'N',
|
|
589
|
+
tls: this.tls.enabled ? 'Y' : 'N',
|
|
590
|
+
pipe: this.pipelining ? 'Y' : 'N',
|
|
591
|
+
errors: this.errors,
|
|
592
|
+
txns: this.tran_count,
|
|
593
|
+
rcpts: `${this.rcpt_count.accept}/${this.rcpt_count.tempfail}/${this.rcpt_count.reject}`,
|
|
594
|
+
msgs: `${this.msg_count.accept}/${this.msg_count.tempfail}/${this.msg_count.reject}`,
|
|
595
|
+
bytes: this.totalbytes,
|
|
596
|
+
lr: this.last_reject ? this.last_reject : '',
|
|
597
|
+
time: (Date.now() - this.start_time) / 1000,
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
this.results.add(
|
|
601
|
+
{ name: 'disconnect' },
|
|
602
|
+
{
|
|
603
|
+
duration: (Date.now() - this.start_time) / 1000,
|
|
604
|
+
},
|
|
605
|
+
)
|
|
606
|
+
this.lognotice('disconnect', logdetail)
|
|
607
|
+
this.state = states.DISCONNECTED
|
|
608
|
+
this.client.end()
|
|
609
|
+
}
|
|
610
|
+
get_capabilities() {
|
|
611
|
+
return []
|
|
612
|
+
}
|
|
613
|
+
tran_uuid() {
|
|
614
|
+
this.tran_count++
|
|
615
|
+
return `${this.uuid}.${this.tran_count}`
|
|
616
|
+
}
|
|
617
|
+
reset_transaction(cb) {
|
|
618
|
+
this.results.add(
|
|
619
|
+
{ name: 'reset' },
|
|
620
|
+
{
|
|
621
|
+
duration: (Date.now() - this.start_time) / 1000,
|
|
622
|
+
},
|
|
623
|
+
)
|
|
624
|
+
if (this.transaction && this.transaction.resetting === false) {
|
|
625
|
+
// Pause connection to allow the hook to complete
|
|
626
|
+
this.pause()
|
|
627
|
+
this.transaction.resetting = true
|
|
628
|
+
plugins.run_hooks('reset_transaction', this, cb)
|
|
629
|
+
} else {
|
|
630
|
+
this.transaction = null
|
|
631
|
+
if (cb) cb()
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
reset_transaction_respond(retval, msg, cb) {
|
|
635
|
+
if (this.transaction) {
|
|
636
|
+
this.transaction.message_stream.destroy()
|
|
637
|
+
this.transaction = null
|
|
638
|
+
}
|
|
639
|
+
if (cb) cb()
|
|
640
|
+
// Allow the connection to continue
|
|
641
|
+
this.resume()
|
|
642
|
+
}
|
|
643
|
+
init_transaction(cb) {
|
|
644
|
+
this.reset_transaction(() => {
|
|
645
|
+
this.transaction = trans.createTransaction(this.tran_uuid(), cfg)
|
|
646
|
+
// Catch any errors from the message_stream
|
|
647
|
+
this.transaction.message_stream.on('error', (err) => {
|
|
648
|
+
this.logcrit(`message_stream error: ${err.message}`)
|
|
649
|
+
this.respond('421', 'Internal Server Error', () => {
|
|
650
|
+
this.disconnect()
|
|
651
|
+
})
|
|
652
|
+
})
|
|
653
|
+
this.transaction.results = new ResultStore(this)
|
|
654
|
+
if (cb) cb()
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
loop_respond(code, msg) {
|
|
658
|
+
if (this.state >= states.DISCONNECTING) return
|
|
659
|
+
this.state = states.LOOP
|
|
660
|
+
this.loop_code = code
|
|
661
|
+
this.loop_msg = msg
|
|
662
|
+
this.respond(code, msg)
|
|
663
|
+
}
|
|
664
|
+
pause() {
|
|
665
|
+
if (this.state >= states.DISCONNECTING) return
|
|
666
|
+
this.client.pause()
|
|
667
|
+
if (this.state !== states.PAUSE_DATA) this.prev_state = this.state
|
|
668
|
+
this.state = states.PAUSE_DATA
|
|
669
|
+
}
|
|
670
|
+
resume() {
|
|
671
|
+
if (this.state >= states.DISCONNECTING) return
|
|
672
|
+
this.client.resume()
|
|
673
|
+
if (this.prev_state && this.state === states.PAUSE_DATA) {
|
|
674
|
+
this.state = this.prev_state
|
|
675
|
+
}
|
|
676
|
+
this.prev_state = null
|
|
677
|
+
setImmediate(() => this._process_data())
|
|
678
|
+
}
|
|
679
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
680
|
+
// SMTP Responses
|
|
681
|
+
connect_init_respond() {
|
|
682
|
+
this.logdebug('running connect_init_respond')
|
|
683
|
+
plugins.run_hooks('lookup_rdns', this)
|
|
684
|
+
}
|
|
685
|
+
lookup_rdns_respond(retval, msg) {
|
|
686
|
+
switch (retval) {
|
|
687
|
+
case constants.ok:
|
|
688
|
+
this.set('remote', 'host', msg || 'Unknown')
|
|
689
|
+
this.set('remote', 'info', this.remote.info || this.remote.host)
|
|
690
|
+
plugins.run_hooks('connect', this)
|
|
691
|
+
break
|
|
692
|
+
case constants.deny:
|
|
693
|
+
this.loop_respond(554, msg || 'rDNS Lookup Failed')
|
|
694
|
+
break
|
|
695
|
+
case constants.denydisconnect:
|
|
696
|
+
case constants.disconnect:
|
|
697
|
+
this.respond(554, msg || 'rDNS Lookup Failed', () => {
|
|
698
|
+
this.disconnect()
|
|
699
|
+
})
|
|
700
|
+
break
|
|
701
|
+
case constants.denysoft:
|
|
702
|
+
this.loop_respond(421, msg || 'rDNS Temporary Failure')
|
|
703
|
+
break
|
|
704
|
+
case constants.denysoftdisconnect:
|
|
705
|
+
this.respond(421, msg || 'rDNS Temporary Failure', () => {
|
|
706
|
+
this.disconnect()
|
|
707
|
+
})
|
|
708
|
+
break
|
|
709
|
+
default:
|
|
710
|
+
// BUG: dns.reverse throws on invalid input (and sometimes valid
|
|
711
|
+
// input nodejs/node#47847). Also throws when empty results
|
|
712
|
+
try {
|
|
713
|
+
dns.reverse(this.remote.ip, (err, domains) => {
|
|
714
|
+
this.rdns_response(err, domains)
|
|
715
|
+
})
|
|
716
|
+
} catch (err) {
|
|
717
|
+
this.rdns_response(err, [])
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
rdns_response(err, domains) {
|
|
722
|
+
if (err) {
|
|
723
|
+
switch (err.code) {
|
|
724
|
+
case dns.NXDOMAIN:
|
|
725
|
+
case dns.NOTFOUND:
|
|
726
|
+
this.set('remote', 'host', 'NXDOMAIN')
|
|
727
|
+
break
|
|
728
|
+
default:
|
|
729
|
+
this.set('remote', 'host', 'DNSERROR')
|
|
730
|
+
break
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
733
|
+
this.set('remote', 'host', domains[0] || 'Unknown')
|
|
734
|
+
this.results.add({ name: 'remote' }, this.remote)
|
|
735
|
+
}
|
|
736
|
+
this.set('remote', 'info', this.remote.info || this.remote.host)
|
|
737
|
+
plugins.run_hooks('connect', this)
|
|
738
|
+
}
|
|
739
|
+
unrecognized_command_respond(retval, msg) {
|
|
740
|
+
switch (retval) {
|
|
741
|
+
case constants.ok:
|
|
742
|
+
// response already sent, cool...
|
|
743
|
+
break
|
|
744
|
+
case constants.next_hook:
|
|
745
|
+
plugins.run_hooks(msg, this)
|
|
746
|
+
break
|
|
747
|
+
case constants.deny:
|
|
748
|
+
this.respond(500, msg || 'Unrecognized command')
|
|
749
|
+
break
|
|
750
|
+
case constants.denydisconnect:
|
|
751
|
+
case constants.denysoftdisconnect:
|
|
752
|
+
this.respond(retval === constants.denydisconnect ? 521 : 421, msg || 'Unrecognized command', () => {
|
|
753
|
+
this.disconnect()
|
|
754
|
+
})
|
|
755
|
+
break
|
|
756
|
+
default:
|
|
757
|
+
this.errors++
|
|
758
|
+
this.respond(500, msg || 'Unrecognized command')
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
connect_respond(retval, msg) {
|
|
762
|
+
// RFC 5321 Section 4.3.2 states that the only valid SMTP codes here are:
|
|
763
|
+
// 220 = Service ready
|
|
764
|
+
// 554 = Transaction failed (no SMTP service here)
|
|
765
|
+
// 421 = Service shutting down and closing transmission channel
|
|
766
|
+
switch (retval) {
|
|
767
|
+
case constants.deny:
|
|
768
|
+
this.loop_respond(554, msg || 'Your mail is not welcome here')
|
|
769
|
+
break
|
|
770
|
+
case constants.denydisconnect:
|
|
771
|
+
case constants.disconnect:
|
|
772
|
+
this.respond(554, msg || 'Your mail is not welcome here', () => {
|
|
773
|
+
this.disconnect()
|
|
774
|
+
})
|
|
775
|
+
break
|
|
776
|
+
case constants.denysoft:
|
|
777
|
+
this.loop_respond(421, msg || 'Come back later')
|
|
778
|
+
break
|
|
779
|
+
case constants.denysoftdisconnect:
|
|
780
|
+
this.respond(421, msg || 'Come back later', () => {
|
|
781
|
+
this.disconnect()
|
|
782
|
+
})
|
|
783
|
+
break
|
|
784
|
+
default: {
|
|
785
|
+
let greeting
|
|
786
|
+
if (cfg.message.greeting?.length) {
|
|
787
|
+
// RFC5321 section 4.2
|
|
788
|
+
// Hostname/domain should appear after the 220
|
|
789
|
+
greeting = [...cfg.message.greeting]
|
|
790
|
+
greeting[0] = `${this.local.host} ESMTP ${greeting[0]}`
|
|
791
|
+
if (cfg.uuid.banner_chars) {
|
|
792
|
+
greeting[0] += ` (${this.uuid.slice(0, cfg.uuid.banner_chars)})`
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
greeting = `${this.local.host} ESMTP ${this.local.info} ready`
|
|
796
|
+
if (cfg.uuid.banner_chars) {
|
|
797
|
+
greeting += ` (${this.uuid.slice(0, cfg.uuid.banner_chars)})`
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
this.respond(220, msg || greeting)
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
get_remote(prop) {
|
|
805
|
+
switch (this.remote[prop]) {
|
|
806
|
+
case 'NXDOMAIN':
|
|
807
|
+
case 'DNSERROR':
|
|
808
|
+
case '':
|
|
809
|
+
case undefined:
|
|
810
|
+
case null:
|
|
811
|
+
return `[${this.remote.ip}]`
|
|
812
|
+
default:
|
|
813
|
+
return `${this.remote[prop]} [${this.remote.ip}]`
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
helo_respond(retval, msg) {
|
|
817
|
+
switch (retval) {
|
|
818
|
+
case constants.deny:
|
|
819
|
+
this.respond(550, msg || 'HELO denied', () => {
|
|
820
|
+
this.set('hello', 'verb', null)
|
|
821
|
+
this.set('hello', 'host', null)
|
|
822
|
+
})
|
|
823
|
+
break
|
|
824
|
+
case constants.denydisconnect:
|
|
825
|
+
this.respond(550, msg || 'HELO denied', () => {
|
|
826
|
+
this.disconnect()
|
|
827
|
+
})
|
|
828
|
+
break
|
|
829
|
+
case constants.denysoft:
|
|
830
|
+
this.respond(450, msg || 'HELO denied', () => {
|
|
831
|
+
this.set('hello', 'verb', null)
|
|
832
|
+
this.set('hello', 'host', null)
|
|
833
|
+
})
|
|
834
|
+
break
|
|
835
|
+
case constants.denysoftdisconnect:
|
|
836
|
+
this.respond(450, msg || 'HELO denied', () => {
|
|
837
|
+
this.disconnect()
|
|
838
|
+
})
|
|
839
|
+
break
|
|
840
|
+
default:
|
|
841
|
+
// RFC5321 section 4.1.1.1
|
|
842
|
+
// Hostname/domain should appear after 250
|
|
843
|
+
this.respond(250, `${this.local.host} Hello ${this.get_remote('host')}, ${cfg.message.helo}`)
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
ehlo_respond(retval, msg) {
|
|
847
|
+
switch (retval) {
|
|
848
|
+
case constants.deny:
|
|
849
|
+
this.respond(550, msg || 'EHLO denied', () => {
|
|
850
|
+
this.set('hello', 'verb', null)
|
|
851
|
+
this.set('hello', 'host', null)
|
|
852
|
+
})
|
|
853
|
+
break
|
|
854
|
+
case constants.denydisconnect:
|
|
855
|
+
this.respond(550, msg || 'EHLO denied', () => {
|
|
856
|
+
this.disconnect()
|
|
857
|
+
})
|
|
858
|
+
break
|
|
859
|
+
case constants.denysoft:
|
|
860
|
+
this.respond(450, msg || 'EHLO denied', () => {
|
|
861
|
+
this.set('hello', 'verb', null)
|
|
862
|
+
this.set('hello', 'host', null)
|
|
863
|
+
})
|
|
864
|
+
break
|
|
865
|
+
case constants.denysoftdisconnect:
|
|
866
|
+
this.respond(450, msg || 'EHLO denied', () => {
|
|
867
|
+
this.disconnect()
|
|
868
|
+
})
|
|
869
|
+
break
|
|
870
|
+
default: {
|
|
871
|
+
// RFC5321 section 4.1.1.1
|
|
872
|
+
// Hostname/domain should appear after 250
|
|
873
|
+
|
|
874
|
+
const response = [
|
|
875
|
+
`${this.local.host} Hello ${this.get_remote('host')}, ${cfg.message.helo}`,
|
|
876
|
+
'PIPELINING',
|
|
877
|
+
'8BITMIME',
|
|
878
|
+
]
|
|
879
|
+
|
|
880
|
+
if (cfg.main.smtputf8) response.push('SMTPUTF8')
|
|
881
|
+
|
|
882
|
+
response.push(`SIZE ${cfg.max.bytes}`)
|
|
883
|
+
|
|
884
|
+
this.capabilities = response
|
|
885
|
+
|
|
886
|
+
plugins.run_hooks('capabilities', this)
|
|
887
|
+
this.esmtp = true
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
capabilities_respond() {
|
|
892
|
+
this.respond(250, this.capabilities)
|
|
893
|
+
}
|
|
894
|
+
quit_respond(retval, msg) {
|
|
895
|
+
this.respond(221, msg || `${this.local.host} ${cfg.message.close}`, () => {
|
|
896
|
+
this.disconnect()
|
|
897
|
+
})
|
|
898
|
+
}
|
|
899
|
+
vrfy_respond(retval, msg) {
|
|
900
|
+
switch (retval) {
|
|
901
|
+
case constants.deny:
|
|
902
|
+
this.respond(550, msg || 'Access Denied', () => {
|
|
903
|
+
this.reset_transaction()
|
|
904
|
+
})
|
|
905
|
+
break
|
|
906
|
+
case constants.denydisconnect:
|
|
907
|
+
this.respond(550, msg || 'Access Denied', () => {
|
|
908
|
+
this.disconnect()
|
|
909
|
+
})
|
|
910
|
+
break
|
|
911
|
+
case constants.denysoft:
|
|
912
|
+
this.respond(450, msg || 'Lookup Failed', () => {
|
|
913
|
+
this.reset_transaction()
|
|
914
|
+
})
|
|
915
|
+
break
|
|
916
|
+
case constants.denysoftdisconnect:
|
|
917
|
+
this.respond(450, msg || 'Lookup Failed', () => {
|
|
918
|
+
this.disconnect()
|
|
919
|
+
})
|
|
920
|
+
break
|
|
921
|
+
case constants.ok:
|
|
922
|
+
this.respond(250, msg || 'User OK')
|
|
923
|
+
break
|
|
924
|
+
default:
|
|
925
|
+
this.respond(252, "Just try sending a mail and we'll see how it turns out...")
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
noop_respond(retval, msg) {
|
|
929
|
+
switch (retval) {
|
|
930
|
+
case constants.deny:
|
|
931
|
+
this.respond(500, msg || 'Stop wasting my time')
|
|
932
|
+
break
|
|
933
|
+
case constants.denydisconnect:
|
|
934
|
+
this.respond(500, msg || 'Stop wasting my time', () => {
|
|
935
|
+
this.disconnect()
|
|
936
|
+
})
|
|
937
|
+
break
|
|
938
|
+
default:
|
|
939
|
+
this.respond(250, 'OK')
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
rset_respond() {
|
|
943
|
+
this.respond(250, 'OK', () => {
|
|
944
|
+
this.reset_transaction()
|
|
945
|
+
})
|
|
946
|
+
}
|
|
947
|
+
mail_respond(retval, msg) {
|
|
948
|
+
if (!this.transaction) {
|
|
949
|
+
this.logerror('mail_respond found no transaction!')
|
|
950
|
+
return
|
|
951
|
+
}
|
|
952
|
+
const sender = this.transaction.mail_from
|
|
953
|
+
const dmsg = `sender ${sender.format()}`
|
|
954
|
+
this.lognotice(dmsg, {
|
|
955
|
+
code: constants.translate(retval),
|
|
956
|
+
msg: msg || '',
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
const store_results = (action) => {
|
|
960
|
+
let addr = sender.format()
|
|
961
|
+
if (addr.length > 2) {
|
|
962
|
+
// all but null sender
|
|
963
|
+
addr = addr.slice(1, -1) // trim off < >
|
|
964
|
+
}
|
|
965
|
+
this.transaction.results.add(
|
|
966
|
+
{ name: 'mail_from' },
|
|
967
|
+
{
|
|
968
|
+
action,
|
|
969
|
+
code: constants.translate(retval),
|
|
970
|
+
address: addr,
|
|
971
|
+
},
|
|
972
|
+
)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
switch (retval) {
|
|
976
|
+
case constants.deny:
|
|
977
|
+
this.respond(550, msg || `${dmsg} denied`, () => {
|
|
978
|
+
store_results('reject')
|
|
979
|
+
this.reset_transaction()
|
|
980
|
+
})
|
|
981
|
+
break
|
|
982
|
+
case constants.denydisconnect:
|
|
983
|
+
this.respond(550, msg || `${dmsg} denied`, () => {
|
|
984
|
+
store_results('reject')
|
|
985
|
+
this.disconnect()
|
|
986
|
+
})
|
|
987
|
+
break
|
|
988
|
+
case constants.denysoft:
|
|
989
|
+
this.respond(450, msg || `${dmsg} denied`, () => {
|
|
990
|
+
store_results('tempfail')
|
|
991
|
+
this.reset_transaction()
|
|
992
|
+
})
|
|
993
|
+
break
|
|
994
|
+
case constants.denysoftdisconnect:
|
|
995
|
+
this.respond(450, msg || `${dmsg} denied`, () => {
|
|
996
|
+
store_results('tempfail')
|
|
997
|
+
this.disconnect()
|
|
998
|
+
})
|
|
999
|
+
break
|
|
1000
|
+
default:
|
|
1001
|
+
store_results('accept')
|
|
1002
|
+
this.respond(250, msg || `${dmsg} OK`)
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
rcpt_incr(rcpt, action, msg, retval) {
|
|
1006
|
+
this.transaction.rcpt_count[action]++
|
|
1007
|
+
this.rcpt_count[action]++
|
|
1008
|
+
|
|
1009
|
+
const addr = rcpt.format()
|
|
1010
|
+
const recipient = {
|
|
1011
|
+
address: addr.slice(1, -1),
|
|
1012
|
+
action,
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (msg && action !== 'accept') {
|
|
1016
|
+
if (typeof msg === 'object' && msg.constructor.name === 'DSN') {
|
|
1017
|
+
recipient.msg = msg.reply
|
|
1018
|
+
recipient.code = msg.code
|
|
1019
|
+
} else {
|
|
1020
|
+
recipient.msg = msg
|
|
1021
|
+
recipient.code = constants.translate(retval)
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
this.transaction.results.push({ name: 'rcpt_to' }, { recipient })
|
|
1026
|
+
}
|
|
1027
|
+
rcpt_ok_respond(retval, msg) {
|
|
1028
|
+
if (!this.transaction) {
|
|
1029
|
+
this.results.add(this, {
|
|
1030
|
+
err: 'rcpt_ok_respond found no transaction',
|
|
1031
|
+
})
|
|
1032
|
+
return
|
|
1033
|
+
}
|
|
1034
|
+
if (!msg) msg = this.last_rcpt_msg
|
|
1035
|
+
const rcpt = this.transaction.rcpt_to[this.transaction.rcpt_to.length - 1]
|
|
1036
|
+
const dmsg = `recipient ${rcpt.format()}`
|
|
1037
|
+
// Log OK instead of CONT as this hook only runs if hook_rcpt returns OK
|
|
1038
|
+
this.lognotice(dmsg, {
|
|
1039
|
+
code: constants.translate(retval === constants.cont ? constants.ok : retval),
|
|
1040
|
+
msg: msg || '',
|
|
1041
|
+
sender: this.transaction.mail_from.address,
|
|
1042
|
+
})
|
|
1043
|
+
switch (retval) {
|
|
1044
|
+
case constants.deny:
|
|
1045
|
+
this.respond(550, msg || `${dmsg} denied`, () => {
|
|
1046
|
+
this.rcpt_incr(rcpt, 'reject', msg, retval)
|
|
1047
|
+
this.transaction.rcpt_to.pop()
|
|
1048
|
+
})
|
|
1049
|
+
break
|
|
1050
|
+
case constants.denydisconnect:
|
|
1051
|
+
this.respond(550, msg || `${dmsg} denied`, () => {
|
|
1052
|
+
this.rcpt_incr(rcpt, 'reject', msg, retval)
|
|
1053
|
+
this.disconnect()
|
|
1054
|
+
})
|
|
1055
|
+
break
|
|
1056
|
+
case constants.denysoft:
|
|
1057
|
+
this.respond(450, msg || `${dmsg} denied`, () => {
|
|
1058
|
+
this.rcpt_incr(rcpt, 'tempfail', msg, retval)
|
|
1059
|
+
this.transaction.rcpt_to.pop()
|
|
1060
|
+
})
|
|
1061
|
+
break
|
|
1062
|
+
case constants.denysoftdisconnect:
|
|
1063
|
+
this.respond(450, msg || `${dmsg} denied`, () => {
|
|
1064
|
+
this.rcpt_incr(rcpt, 'tempfail', msg, retval)
|
|
1065
|
+
this.disconnect()
|
|
1066
|
+
})
|
|
1067
|
+
break
|
|
1068
|
+
default:
|
|
1069
|
+
this.respond(250, msg || `${dmsg} OK`, () => {
|
|
1070
|
+
this.rcpt_incr(rcpt, 'accept', msg, retval)
|
|
1071
|
+
})
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
rcpt_respond(retval, msg) {
|
|
1075
|
+
if (retval === constants.cont && this.relaying) {
|
|
1076
|
+
retval = constants.ok
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (!this.transaction) {
|
|
1080
|
+
this.results.add(this, {
|
|
1081
|
+
err: 'rcpt_respond found no transaction',
|
|
1082
|
+
})
|
|
1083
|
+
return
|
|
1084
|
+
}
|
|
1085
|
+
const rcpt = this.transaction.rcpt_to[this.transaction.rcpt_to.length - 1]
|
|
1086
|
+
const dmsg = `recipient ${rcpt.format()}`
|
|
1087
|
+
if (retval !== constants.ok) {
|
|
1088
|
+
this.lognotice(dmsg, {
|
|
1089
|
+
code: constants.translate(retval === constants.cont ? constants.ok : retval),
|
|
1090
|
+
msg: msg || '',
|
|
1091
|
+
sender: this.transaction.mail_from.address,
|
|
1092
|
+
})
|
|
1093
|
+
}
|
|
1094
|
+
switch (retval) {
|
|
1095
|
+
case constants.deny:
|
|
1096
|
+
this.respond(550, msg || `${dmsg} denied`, () => {
|
|
1097
|
+
this.rcpt_incr(rcpt, 'reject', msg, retval)
|
|
1098
|
+
this.transaction.rcpt_to.pop()
|
|
1099
|
+
})
|
|
1100
|
+
break
|
|
1101
|
+
case constants.denydisconnect:
|
|
1102
|
+
this.respond(550, msg || `${dmsg} denied`, () => {
|
|
1103
|
+
this.rcpt_incr(rcpt, 'reject', msg, retval)
|
|
1104
|
+
this.disconnect()
|
|
1105
|
+
})
|
|
1106
|
+
break
|
|
1107
|
+
case constants.denysoft:
|
|
1108
|
+
this.respond(450, msg || `${dmsg} denied`, () => {
|
|
1109
|
+
this.rcpt_incr(rcpt, 'tempfail', msg, retval)
|
|
1110
|
+
this.transaction.rcpt_to.pop()
|
|
1111
|
+
})
|
|
1112
|
+
break
|
|
1113
|
+
case constants.denysoftdisconnect:
|
|
1114
|
+
this.respond(450, msg || `${dmsg} denied`, () => {
|
|
1115
|
+
this.rcpt_incr(rcpt, 'tempfail', msg, retval)
|
|
1116
|
+
this.disconnect()
|
|
1117
|
+
})
|
|
1118
|
+
break
|
|
1119
|
+
case constants.ok:
|
|
1120
|
+
// Store any msg for rcpt_ok
|
|
1121
|
+
this.last_rcpt_msg = msg
|
|
1122
|
+
plugins.run_hooks('rcpt_ok', this, rcpt)
|
|
1123
|
+
break
|
|
1124
|
+
default: {
|
|
1125
|
+
if (retval !== constants.cont) {
|
|
1126
|
+
this.logalert('No plugin determined if relaying was allowed')
|
|
1127
|
+
}
|
|
1128
|
+
const rej_msg = `I cannot deliver mail for ${rcpt.format()}`
|
|
1129
|
+
this.respond(550, rej_msg, () => {
|
|
1130
|
+
this.rcpt_incr(rcpt, 'reject', rej_msg, retval)
|
|
1131
|
+
this.transaction.rcpt_to.pop()
|
|
1132
|
+
})
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
1137
|
+
// HAProxy support
|
|
1138
|
+
|
|
1139
|
+
apply_proxy(proxy) {
|
|
1140
|
+
if (this.proxy.timer) {
|
|
1141
|
+
clearTimeout(this.proxy.timer)
|
|
1142
|
+
this.proxy.timer = null
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const { proto, src_ip, src_port, dst_ip, dst_port } = proxy
|
|
1146
|
+
const proxy_ip = proxy.proxy_ip || this.remote.ip
|
|
1147
|
+
|
|
1148
|
+
// Apply changes
|
|
1149
|
+
this.loginfo('HAProxy', {
|
|
1150
|
+
proto,
|
|
1151
|
+
src_ip: `${src_ip}:${src_port}`,
|
|
1152
|
+
dst_ip: `${dst_ip}:${dst_port}`,
|
|
1153
|
+
})
|
|
1154
|
+
|
|
1155
|
+
this.notes.proxy = {
|
|
1156
|
+
type: 'haproxy',
|
|
1157
|
+
proto,
|
|
1158
|
+
src_ip,
|
|
1159
|
+
src_port,
|
|
1160
|
+
dst_ip,
|
|
1161
|
+
dst_port,
|
|
1162
|
+
proxy_ip,
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
this.reset_transaction(() => {
|
|
1166
|
+
this.set('proxy.ip', proxy_ip)
|
|
1167
|
+
this.set('proxy.type', 'haproxy')
|
|
1168
|
+
this.relaying = false
|
|
1169
|
+
this.set('local.ip', dst_ip)
|
|
1170
|
+
this.set('local.port', parseInt(dst_port, 10))
|
|
1171
|
+
this.set('remote.ip', src_ip)
|
|
1172
|
+
this.set('remote.port', parseInt(src_port, 10))
|
|
1173
|
+
this.set('remote.host', null)
|
|
1174
|
+
this.set('hello.host', null)
|
|
1175
|
+
this.results.add({ name: 'local' }, this.local)
|
|
1176
|
+
this.results.add({ name: 'remote' }, this.remote)
|
|
1177
|
+
plugins.run_hooks('connect_init', this)
|
|
1178
|
+
})
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
cmd_proxy(line) {
|
|
1182
|
+
if (!this.proxy.allowed) {
|
|
1183
|
+
this.respond(421, `PROXY not allowed from ${this.remote.ip}`)
|
|
1184
|
+
return this.disconnect()
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const proxy = net_utils.parse_proxy_line(line)
|
|
1188
|
+
if (!proxy) {
|
|
1189
|
+
this.respond(421, 'Invalid PROXY format')
|
|
1190
|
+
return this.disconnect()
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
this.apply_proxy(proxy)
|
|
1194
|
+
}
|
|
1195
|
+
/////////////////////////////////////////////////////////////////////////////
|
|
1196
|
+
// SMTP Commands
|
|
1197
|
+
|
|
1198
|
+
cmd_internalcmd(line) {
|
|
1199
|
+
if (!this.remote.is_local) {
|
|
1200
|
+
return this.respond(501, 'INTERNALCMD not allowed remotely')
|
|
1201
|
+
}
|
|
1202
|
+
const results = String(line).split(/ +/)
|
|
1203
|
+
if (/key:/.test(results[0])) {
|
|
1204
|
+
const internal_key = config.get('internalcmd_key')
|
|
1205
|
+
if (results[0] != `key:${internal_key}`) {
|
|
1206
|
+
return this.respond(501, 'Invalid internalcmd_key - check config')
|
|
1207
|
+
}
|
|
1208
|
+
results.shift()
|
|
1209
|
+
} else if (config.get('internalcmd_key')) {
|
|
1210
|
+
return this.respond(501, 'Missing internalcmd_key - check config')
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Now send the internal command to the master process
|
|
1214
|
+
const command = results.shift()
|
|
1215
|
+
if (!command) {
|
|
1216
|
+
return this.respond(501, 'No command given')
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
require('./server').sendToMaster(command, results)
|
|
1220
|
+
return this.respond(250, 'Command sent for execution. Check Haraka logs for results.')
|
|
1221
|
+
}
|
|
1222
|
+
cmd_helo(line) {
|
|
1223
|
+
const results = String(line).split(/ +/)
|
|
1224
|
+
const host = results[0]
|
|
1225
|
+
if (!host) {
|
|
1226
|
+
return this.respond(501, 'HELO requires domain/address - see RFC-2821 4.1.1.1')
|
|
1227
|
+
}
|
|
1228
|
+
// RFC 5321 §4.1.1.1: the domain/address-literal cannot contain
|
|
1229
|
+
// control characters. process_line() only strips the first \r?\n,
|
|
1230
|
+
// so a bare \r could otherwise survive into hello.host and the
|
|
1231
|
+
// generated Received: header / logs (header injection).
|
|
1232
|
+
if (/[\x00-\x1f\x7f]/.test(host)) {
|
|
1233
|
+
return this.respond(501, 'HELO syntax error - see RFC-2821 4.1.1.1')
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
this.reset_transaction(() => {
|
|
1237
|
+
this.set('hello', 'verb', 'HELO')
|
|
1238
|
+
this.set('hello', 'host', host)
|
|
1239
|
+
this.results.add({ name: 'helo' }, this.hello)
|
|
1240
|
+
plugins.run_hooks('helo', this, host)
|
|
1241
|
+
})
|
|
1242
|
+
}
|
|
1243
|
+
cmd_ehlo(line) {
|
|
1244
|
+
const results = String(line).split(/ +/)
|
|
1245
|
+
const host = results[0]
|
|
1246
|
+
if (!host) {
|
|
1247
|
+
return this.respond(501, 'EHLO requires domain/address - see RFC-2821 4.1.1.1')
|
|
1248
|
+
}
|
|
1249
|
+
// RFC 5321 §4.1.1.1: reject control chars (see cmd_helo).
|
|
1250
|
+
if (/[\x00-\x1f\x7f]/.test(host)) {
|
|
1251
|
+
return this.respond(501, 'EHLO syntax error - see RFC-2821 4.1.1.1')
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
this.reset_transaction(() => {
|
|
1255
|
+
this.set('hello', 'verb', 'EHLO')
|
|
1256
|
+
this.set('hello', 'host', host)
|
|
1257
|
+
this.results.add({ name: 'helo' }, this.hello)
|
|
1258
|
+
plugins.run_hooks('ehlo', this, host)
|
|
1259
|
+
})
|
|
1260
|
+
}
|
|
1261
|
+
cmd_quit(args) {
|
|
1262
|
+
// RFC 5321 Section 4.3.2
|
|
1263
|
+
// QUIT does not accept arguments
|
|
1264
|
+
if (args) {
|
|
1265
|
+
return this.respond(501, 'Syntax error')
|
|
1266
|
+
}
|
|
1267
|
+
plugins.run_hooks('quit', this)
|
|
1268
|
+
}
|
|
1269
|
+
cmd_rset(args) {
|
|
1270
|
+
// RFC 5321 Section 4.3.2
|
|
1271
|
+
// RSET does not accept arguments
|
|
1272
|
+
if (args) {
|
|
1273
|
+
return this.respond(501, 'Syntax error')
|
|
1274
|
+
}
|
|
1275
|
+
plugins.run_hooks('rset', this)
|
|
1276
|
+
}
|
|
1277
|
+
cmd_vrfy() {
|
|
1278
|
+
// only supported via plugins
|
|
1279
|
+
plugins.run_hooks('vrfy', this)
|
|
1280
|
+
}
|
|
1281
|
+
cmd_noop() {
|
|
1282
|
+
plugins.run_hooks('noop', this)
|
|
1283
|
+
}
|
|
1284
|
+
cmd_help() {
|
|
1285
|
+
this.respond(250, 'Not implemented')
|
|
1286
|
+
}
|
|
1287
|
+
cmd_mail(line) {
|
|
1288
|
+
if (!this.hello.host) {
|
|
1289
|
+
this.errors++
|
|
1290
|
+
return this.respond(503, 'Use EHLO/HELO before MAIL')
|
|
1291
|
+
}
|
|
1292
|
+
// Require authentication on ports 587 & 465
|
|
1293
|
+
if (!this.relaying && [587, 465].includes(this.local.port)) {
|
|
1294
|
+
this.errors++
|
|
1295
|
+
return this.respond(550, 'Authentication required')
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
let results
|
|
1299
|
+
try {
|
|
1300
|
+
results = rfc1869.parse('mail', line, !this.relaying && cfg.main.strict_rfc1869)
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
this.errors++
|
|
1303
|
+
if (err.stack) {
|
|
1304
|
+
this.lognotice(err.stack.split(/\n/)[0])
|
|
1305
|
+
} else {
|
|
1306
|
+
this.logerror(err)
|
|
1307
|
+
}
|
|
1308
|
+
// Explicitly handle out-of-disk space errors
|
|
1309
|
+
if (err.code === 'ENOSPC') {
|
|
1310
|
+
return this.respond(452, 'Internal Server Error')
|
|
1311
|
+
} else {
|
|
1312
|
+
return this.respond(501, ['Command parsing failed', err])
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
let from
|
|
1317
|
+
const from_raw = results.shift()
|
|
1318
|
+
try {
|
|
1319
|
+
from = new Address(from_raw, { postel: cfg.main.postel })
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
const msg = `Invalid MAIL FROM address ${utils.sanitize(from_raw)}: ${err.message}`
|
|
1322
|
+
this.lognotice(msg)
|
|
1323
|
+
return this.respond(501, msg)
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Get rest of key=value pairs
|
|
1327
|
+
const params = {}
|
|
1328
|
+
for (const param of results) {
|
|
1329
|
+
const kv = param.match(/^([^=]+)(?:=(.+))?$/)
|
|
1330
|
+
if (kv) params[kv[1].toUpperCase()] = kv[2] || null
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Parameters are only valid if EHLO was sent
|
|
1334
|
+
if (!this.esmtp && Object.keys(params).length > 0) {
|
|
1335
|
+
return this.respond(555, 'Invalid command parameters')
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Handle SIZE extension
|
|
1339
|
+
if (params?.SIZE && params.SIZE > 0) {
|
|
1340
|
+
if (cfg.max.bytes > 0 && params.SIZE > cfg.max.bytes) {
|
|
1341
|
+
return this.respond(550, 'Message too big!')
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
this.init_transaction(() => {
|
|
1346
|
+
this.transaction.mail_from = from
|
|
1347
|
+
if (this.hello.verb == 'HELO') {
|
|
1348
|
+
this.transaction.encoding = 'binary'
|
|
1349
|
+
this.encoding = 'binary'
|
|
1350
|
+
}
|
|
1351
|
+
plugins.run_hooks('mail', this, [from, params])
|
|
1352
|
+
})
|
|
1353
|
+
}
|
|
1354
|
+
cmd_rcpt(line) {
|
|
1355
|
+
if (!this.transaction || !this.transaction.mail_from) {
|
|
1356
|
+
this.errors++
|
|
1357
|
+
return this.respond(503, 'Use MAIL before RCPT')
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
let results
|
|
1361
|
+
try {
|
|
1362
|
+
results = rfc1869.parse('rcpt', line, cfg.main.strict_rfc1869 && !this.relaying)
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
this.errors++
|
|
1365
|
+
if (err.stack) {
|
|
1366
|
+
this.lognotice(err.stack.split(/\n/)[0])
|
|
1367
|
+
} else {
|
|
1368
|
+
this.logerror(err)
|
|
1369
|
+
}
|
|
1370
|
+
// Explicitly handle out-of-disk space errors
|
|
1371
|
+
if (err.code === 'ENOSPC') {
|
|
1372
|
+
return this.respond(452, 'Internal Server Error')
|
|
1373
|
+
} else {
|
|
1374
|
+
return this.respond(501, ['Command parsing failed', err])
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
let recip
|
|
1379
|
+
const recip_raw = results.shift()
|
|
1380
|
+
try {
|
|
1381
|
+
recip = new Address(recip_raw, { postel: cfg.main.postel })
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
const msg = `Invalid RCPT TO address ${utils.sanitize(recip_raw)}: ${err.message}`
|
|
1384
|
+
this.lognotice(msg)
|
|
1385
|
+
return this.respond(501, msg)
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Get rest of key=value pairs
|
|
1389
|
+
const params = {}
|
|
1390
|
+
for (const param of results) {
|
|
1391
|
+
const kv = param.match(/^([^=]+)(?:=(.+))?$/)
|
|
1392
|
+
if (kv) params[kv[1].toUpperCase()] = kv[2] || null
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Parameters are only valid if EHLO was sent
|
|
1396
|
+
if (!this.esmtp && Object.keys(params).length > 0) {
|
|
1397
|
+
return this.respond(555, 'Invalid command parameters')
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
this.transaction.rcpt_to.push(recip)
|
|
1401
|
+
plugins.run_hooks('rcpt', this, [recip, params])
|
|
1402
|
+
}
|
|
1403
|
+
received_line() {
|
|
1404
|
+
let smtp = this.hello.verb === 'EHLO' ? 'ESMTP' : 'SMTP'
|
|
1405
|
+
// Implement RFC3848
|
|
1406
|
+
if (this.tls.enabled) smtp += 'S'
|
|
1407
|
+
if (this.authheader) smtp += 'A'
|
|
1408
|
+
|
|
1409
|
+
let sslheader
|
|
1410
|
+
|
|
1411
|
+
if (this.get('tls.cipher.version')) {
|
|
1412
|
+
// standardName appeared in Node.js v12.16 and v13.4
|
|
1413
|
+
// RFC 8314
|
|
1414
|
+
sslheader = `tls ${this.tls.cipher.standardName || this.tls.cipher.name}`
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
let received_header = `from ${this.hello.host} (${this.get_remote('info')})\r
|
|
1418
|
+
\tby ${this.local.host} (${this.local.info}) with ${smtp} id ${this.transaction.uuid}\r
|
|
1419
|
+
\tenvelope-from ${this.transaction.mail_from.format()}`
|
|
1420
|
+
|
|
1421
|
+
if (sslheader) received_header += `\r\n\t${sslheader.replace(/\r?\n\t?$/, '')}`
|
|
1422
|
+
|
|
1423
|
+
// Does not follow RFC 5321 section 4.4 grammar
|
|
1424
|
+
if (this.authheader) received_header += ` ${this.authheader.replace(/\r?\n\t?$/, '')}`
|
|
1425
|
+
|
|
1426
|
+
received_header += `;\r\n\t${utils.date_to_str(new Date())}`
|
|
1427
|
+
|
|
1428
|
+
return received_header
|
|
1429
|
+
}
|
|
1430
|
+
auth_results(message) {
|
|
1431
|
+
// https://datatracker.ietf.org/doc/rfc7001/
|
|
1432
|
+
const has_tran = !!this.transaction?.notes
|
|
1433
|
+
|
|
1434
|
+
// initialize connection note
|
|
1435
|
+
if (!this.notes.authentication_results) {
|
|
1436
|
+
this.notes.authentication_results = []
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// initialize transaction note, if possible
|
|
1440
|
+
if (has_tran === true && !this.transaction.notes.authentication_results) {
|
|
1441
|
+
this.transaction.notes.authentication_results = []
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// Strip CR/LF and other control chars: an attacker-influenced
|
|
1445
|
+
// value (e.g. a failed AUTH username, see auth_base) must not be
|
|
1446
|
+
// able to inject extra header lines into Authentication-Results.
|
|
1447
|
+
// The legitimate folding (;\r\n\t) is added by the join below.
|
|
1448
|
+
// if message, store it in the appropriate note
|
|
1449
|
+
if (message) {
|
|
1450
|
+
if (has_tran === true) {
|
|
1451
|
+
this.transaction.notes.authentication_results.push(utils.sanitize(message))
|
|
1452
|
+
} else {
|
|
1453
|
+
this.notes.authentication_results.push(utils.sanitize(message))
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// assemble the new header
|
|
1458
|
+
let header = [utils.sanitize(this.local.host), ...this.notes.authentication_results]
|
|
1459
|
+
if (has_tran === true) {
|
|
1460
|
+
header = [...header, ...this.transaction.notes.authentication_results]
|
|
1461
|
+
}
|
|
1462
|
+
if (header.length === 1) return '' // no results
|
|
1463
|
+
return header.join(';\r\n\t')
|
|
1464
|
+
}
|
|
1465
|
+
auth_results_clean() {
|
|
1466
|
+
// move any existing Auth-Res headers to Original-Auth-Res headers
|
|
1467
|
+
// http://tools.ietf.org/html/draft-kucherawy-original-authres-00.html
|
|
1468
|
+
const ars = this.transaction.header.get_all('Authentication-Results')
|
|
1469
|
+
if (ars.length === 0) return
|
|
1470
|
+
|
|
1471
|
+
for (const element of ars) {
|
|
1472
|
+
this.transaction.add_header('Original-Authentication-Results', element)
|
|
1473
|
+
}
|
|
1474
|
+
this.transaction.remove_header('Authentication-Results')
|
|
1475
|
+
this.logdebug('Authentication-Results moved to Original-Authentication-Results')
|
|
1476
|
+
}
|
|
1477
|
+
cmd_data(args) {
|
|
1478
|
+
// RFC 5321 Section 4.3.2
|
|
1479
|
+
// DATA does not accept arguments
|
|
1480
|
+
if (args) {
|
|
1481
|
+
this.errors++
|
|
1482
|
+
return this.respond(501, 'Syntax error')
|
|
1483
|
+
}
|
|
1484
|
+
if (!this.transaction) {
|
|
1485
|
+
this.errors++
|
|
1486
|
+
return this.respond(503, 'MAIL required first')
|
|
1487
|
+
}
|
|
1488
|
+
if (!this.transaction.rcpt_to.length) {
|
|
1489
|
+
if (this.pipelining) {
|
|
1490
|
+
return this.respond(554, 'No valid recipients')
|
|
1491
|
+
}
|
|
1492
|
+
this.errors++
|
|
1493
|
+
return this.respond(503, 'RCPT required first')
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
if (cfg.headers.add_received) {
|
|
1497
|
+
this.accumulate_data(`Received: ${this.received_line()}\r\n`)
|
|
1498
|
+
}
|
|
1499
|
+
plugins.run_hooks('data', this)
|
|
1500
|
+
}
|
|
1501
|
+
data_respond(retval, msg) {
|
|
1502
|
+
let cont = 0
|
|
1503
|
+
switch (retval) {
|
|
1504
|
+
case constants.deny:
|
|
1505
|
+
this.respond(554, msg || 'Message denied', () => {
|
|
1506
|
+
this.reset_transaction()
|
|
1507
|
+
})
|
|
1508
|
+
break
|
|
1509
|
+
case constants.denydisconnect:
|
|
1510
|
+
this.respond(554, msg || 'Message denied', () => {
|
|
1511
|
+
this.disconnect()
|
|
1512
|
+
})
|
|
1513
|
+
break
|
|
1514
|
+
case constants.denysoft:
|
|
1515
|
+
this.respond(451, msg || 'Message denied', () => {
|
|
1516
|
+
this.reset_transaction()
|
|
1517
|
+
})
|
|
1518
|
+
break
|
|
1519
|
+
case constants.denysoftdisconnect:
|
|
1520
|
+
this.respond(451, msg || 'Message denied', () => {
|
|
1521
|
+
this.disconnect()
|
|
1522
|
+
})
|
|
1523
|
+
break
|
|
1524
|
+
default:
|
|
1525
|
+
cont = 1
|
|
1526
|
+
}
|
|
1527
|
+
if (!cont) return
|
|
1528
|
+
|
|
1529
|
+
// We already checked for MAIL/RCPT in cmd_data
|
|
1530
|
+
this.respond(354, 'go ahead, make my day', () => {
|
|
1531
|
+
// OK... now we get the data
|
|
1532
|
+
this.state = states.DATA
|
|
1533
|
+
this.transaction.data_bytes = 0
|
|
1534
|
+
})
|
|
1535
|
+
}
|
|
1536
|
+
accumulate_data(line) {
|
|
1537
|
+
this.transaction.data_bytes += line.length
|
|
1538
|
+
|
|
1539
|
+
// Look for .\r\n
|
|
1540
|
+
if (line.length === 3 && line[0] === 0x2e && line[1] === 0x0d && line[2] === 0x0a) {
|
|
1541
|
+
this.data_done()
|
|
1542
|
+
return
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// Look for .\n
|
|
1546
|
+
if (line.length === 2 && line[0] === 0x2e && line[1] === 0x0a) {
|
|
1547
|
+
this.lognotice('Client sent bare line-feed - .\\n rather than .\\r\\n')
|
|
1548
|
+
this.respond(451, 'Bare line-feed; see http://haraka.github.io/barelf/', () => {
|
|
1549
|
+
this.reset_transaction()
|
|
1550
|
+
})
|
|
1551
|
+
return
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// Stop accumulating data as we're going to reject at dot.
|
|
1555
|
+
if (cfg.max.bytes && this.transaction.data_bytes > cfg.max.bytes) {
|
|
1556
|
+
return
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
if (this.transaction.mime_part_count >= cfg.max.mime_parts) {
|
|
1560
|
+
this.logcrit('Possible DoS attempt - too many MIME parts')
|
|
1561
|
+
this.respond(554, 'Transaction failed due to too many MIME parts', () => {
|
|
1562
|
+
this.disconnect()
|
|
1563
|
+
})
|
|
1564
|
+
return
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
this.transaction.add_data(line)
|
|
1568
|
+
}
|
|
1569
|
+
data_done() {
|
|
1570
|
+
this.pause()
|
|
1571
|
+
this.totalbytes += this.transaction.data_bytes
|
|
1572
|
+
|
|
1573
|
+
// Check message size limit
|
|
1574
|
+
if (cfg.max.bytes && this.transaction.data_bytes > cfg.max.bytes) {
|
|
1575
|
+
this.lognotice(`Incoming message exceeded max size of ${cfg.max.bytes}`)
|
|
1576
|
+
return plugins.run_hooks('max_data_exceeded', this)
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Check max received headers count
|
|
1580
|
+
if (this.transaction.header.get_all('received').length > cfg.headers.max_received) {
|
|
1581
|
+
this.logerror('Incoming message had too many Received headers')
|
|
1582
|
+
this.respond(550, 'Too many received headers - possible mail loop', () => {
|
|
1583
|
+
this.reset_transaction()
|
|
1584
|
+
})
|
|
1585
|
+
return
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// Warn if we hit the maximum parsed header lines limit
|
|
1589
|
+
if (this.transaction.header_lines.length >= cfg.headers.max_lines) {
|
|
1590
|
+
this.logwarn(`Incoming message reached maximum parsing limit of ${cfg.headers.max_lines} header lines`)
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
if (cfg.headers.clean_auth_results) {
|
|
1594
|
+
this.auth_results_clean() // rename old A-R headers
|
|
1595
|
+
}
|
|
1596
|
+
const ar_field = this.auth_results() // assemble new one
|
|
1597
|
+
if (ar_field) {
|
|
1598
|
+
this.transaction.add_header('Authentication-Results', ar_field)
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
this.transaction.end_data(() => {
|
|
1602
|
+
// As this will be called asynchronously,
|
|
1603
|
+
// make sure we still have a transaction.
|
|
1604
|
+
if (!this.transaction) return
|
|
1605
|
+
// Record the start time of this hook as we can't take too long
|
|
1606
|
+
// as the client will typically hang up after 2 to 3 minutes
|
|
1607
|
+
// despite the RFC mandating that 10 minutes should be allowed.
|
|
1608
|
+
this.transaction.data_post_start = Date.now()
|
|
1609
|
+
plugins.run_hooks('data_post', this)
|
|
1610
|
+
})
|
|
1611
|
+
}
|
|
1612
|
+
data_post_respond(retval, msg) {
|
|
1613
|
+
if (!this.transaction) return
|
|
1614
|
+
this.transaction.data_post_delay = (Date.now() - this.transaction.data_post_start) / 1000
|
|
1615
|
+
const mid = this.transaction.header.get('Message-ID') || ''
|
|
1616
|
+
this.lognotice('message', {
|
|
1617
|
+
mid: mid.replace(/\r?\n/, ''),
|
|
1618
|
+
size: this.transaction.data_bytes,
|
|
1619
|
+
rcpts: `${this.transaction.rcpt_count.accept}/${this.transaction.rcpt_count.tempfail}/${this.transaction.rcpt_count.reject}`,
|
|
1620
|
+
delay: this.transaction.data_post_delay,
|
|
1621
|
+
code: constants.translate(retval),
|
|
1622
|
+
msg: msg || '',
|
|
1623
|
+
})
|
|
1624
|
+
const ar_field = this.auth_results() // assemble A-R header
|
|
1625
|
+
if (ar_field) {
|
|
1626
|
+
this.transaction.remove_header('Authentication-Results')
|
|
1627
|
+
this.transaction.add_leading_header('Authentication-Results', ar_field)
|
|
1628
|
+
}
|
|
1629
|
+
switch (retval) {
|
|
1630
|
+
case constants.deny:
|
|
1631
|
+
this.respond(550, msg || 'Message denied', () => {
|
|
1632
|
+
this.msg_count.reject++
|
|
1633
|
+
this.transaction.msg_status = 'rejected'
|
|
1634
|
+
this.reset_transaction(() => this.resume())
|
|
1635
|
+
})
|
|
1636
|
+
break
|
|
1637
|
+
case constants.denydisconnect:
|
|
1638
|
+
this.respond(550, msg || 'Message denied', () => {
|
|
1639
|
+
this.msg_count.reject++
|
|
1640
|
+
this.transaction.msg_status = 'rejected'
|
|
1641
|
+
this.disconnect()
|
|
1642
|
+
})
|
|
1643
|
+
break
|
|
1644
|
+
case constants.denysoft:
|
|
1645
|
+
this.respond(450, msg || 'Message denied temporarily', () => {
|
|
1646
|
+
this.msg_count.tempfail++
|
|
1647
|
+
this.transaction.msg_status = 'deferred'
|
|
1648
|
+
this.reset_transaction(() => this.resume())
|
|
1649
|
+
})
|
|
1650
|
+
break
|
|
1651
|
+
case constants.denysoftdisconnect:
|
|
1652
|
+
this.respond(450, msg || 'Message denied temporarily', () => {
|
|
1653
|
+
this.msg_count.tempfail++
|
|
1654
|
+
this.transaction.msg_status = 'deferred'
|
|
1655
|
+
this.disconnect()
|
|
1656
|
+
})
|
|
1657
|
+
break
|
|
1658
|
+
default:
|
|
1659
|
+
if (this.relaying) {
|
|
1660
|
+
plugins.run_hooks('queue_outbound', this)
|
|
1661
|
+
} else {
|
|
1662
|
+
plugins.run_hooks('queue', this)
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
max_data_exceeded_respond(retval) {
|
|
1667
|
+
// TODO: Maybe figure out what to do with other return codes
|
|
1668
|
+
this.respond(retval === constants.denysoft ? 450 : 550, 'Message too big!', () => {
|
|
1669
|
+
this.reset_transaction()
|
|
1670
|
+
})
|
|
1671
|
+
}
|
|
1672
|
+
queue_msg(retval, msg) {
|
|
1673
|
+
if (msg) {
|
|
1674
|
+
if (typeof msg === 'object' && msg.constructor.name === 'DSN') {
|
|
1675
|
+
return msg.reply
|
|
1676
|
+
}
|
|
1677
|
+
return msg
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
switch (retval) {
|
|
1681
|
+
case constants.ok:
|
|
1682
|
+
return 'Message Queued'
|
|
1683
|
+
case constants.deny:
|
|
1684
|
+
case constants.denydisconnect:
|
|
1685
|
+
return 'Message denied'
|
|
1686
|
+
case constants.denysoft:
|
|
1687
|
+
case constants.denysoftdisconnect:
|
|
1688
|
+
return 'Message denied temporarily'
|
|
1689
|
+
default:
|
|
1690
|
+
return ''
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
store_queue_result(retval, msg) {
|
|
1694
|
+
const res_as = { name: 'queue' }
|
|
1695
|
+
switch (retval) {
|
|
1696
|
+
case constants.ok:
|
|
1697
|
+
this.transaction.results.add(res_as, { pass: msg })
|
|
1698
|
+
break
|
|
1699
|
+
case constants.deny:
|
|
1700
|
+
case constants.denydisconnect:
|
|
1701
|
+
case constants.denysoft:
|
|
1702
|
+
case constants.denysoftdisconnect:
|
|
1703
|
+
this.transaction.results.add(res_as, { fail: msg })
|
|
1704
|
+
break
|
|
1705
|
+
case constants.cont:
|
|
1706
|
+
break
|
|
1707
|
+
default:
|
|
1708
|
+
this.transaction.results.add(res_as, { msg })
|
|
1709
|
+
break
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
queue_outbound_respond(retval, msg) {
|
|
1713
|
+
if (this.remote.closed) return
|
|
1714
|
+
msg = this.queue_msg(retval, msg) || 'Message Queued'
|
|
1715
|
+
this.store_queue_result(retval, msg)
|
|
1716
|
+
msg = `${msg} (${this.transaction.uuid})`
|
|
1717
|
+
if (retval !== constants.ok) {
|
|
1718
|
+
this.lognotice('queue', {
|
|
1719
|
+
code: constants.translate(retval),
|
|
1720
|
+
msg,
|
|
1721
|
+
})
|
|
1722
|
+
}
|
|
1723
|
+
switch (retval) {
|
|
1724
|
+
case constants.ok:
|
|
1725
|
+
plugins.run_hooks('queue_ok', this, msg)
|
|
1726
|
+
break
|
|
1727
|
+
case constants.deny:
|
|
1728
|
+
this.respond(550, msg, () => {
|
|
1729
|
+
this.msg_count.reject++
|
|
1730
|
+
this.transaction.msg_status = 'rejected'
|
|
1731
|
+
this.reset_transaction(() => this.resume())
|
|
1732
|
+
})
|
|
1733
|
+
break
|
|
1734
|
+
case constants.denydisconnect:
|
|
1735
|
+
this.respond(550, msg, () => {
|
|
1736
|
+
this.msg_count.reject++
|
|
1737
|
+
this.transaction.msg_status = 'rejected'
|
|
1738
|
+
this.disconnect()
|
|
1739
|
+
})
|
|
1740
|
+
break
|
|
1741
|
+
case constants.denysoft:
|
|
1742
|
+
this.respond(450, msg, () => {
|
|
1743
|
+
this.msg_count.tempfail++
|
|
1744
|
+
this.transaction.msg_status = 'deferred'
|
|
1745
|
+
this.reset_transaction(() => this.resume())
|
|
1746
|
+
})
|
|
1747
|
+
break
|
|
1748
|
+
case constants.denysoftdisconnect:
|
|
1749
|
+
this.respond(450, msg, () => {
|
|
1750
|
+
this.msg_count.tempfail++
|
|
1751
|
+
this.transaction.msg_status = 'deferred'
|
|
1752
|
+
this.disconnect()
|
|
1753
|
+
})
|
|
1754
|
+
break
|
|
1755
|
+
default:
|
|
1756
|
+
outbound.send_trans_email(this.transaction, (retval2, msg2) => {
|
|
1757
|
+
if (!msg2) msg2 = this.queue_msg(retval2, msg)
|
|
1758
|
+
switch (retval2) {
|
|
1759
|
+
case constants.ok:
|
|
1760
|
+
if (!msg2) msg2 = this.queue_msg(retval2, msg2)
|
|
1761
|
+
plugins.run_hooks('queue_ok', this, msg2)
|
|
1762
|
+
break
|
|
1763
|
+
case constants.deny:
|
|
1764
|
+
if (!msg2) msg2 = this.queue_msg(retval2, msg2)
|
|
1765
|
+
this.respond(550, msg2, () => {
|
|
1766
|
+
this.msg_count.reject++
|
|
1767
|
+
this.transaction.msg_status = 'rejected'
|
|
1768
|
+
this.reset_transaction(() => {
|
|
1769
|
+
this.resume()
|
|
1770
|
+
})
|
|
1771
|
+
})
|
|
1772
|
+
break
|
|
1773
|
+
default:
|
|
1774
|
+
this.logerror(`Unrecognized response from outbound layer: ${retval2} : ${msg2}`)
|
|
1775
|
+
this.respond(550, msg2 || 'Internal Server Error', () => {
|
|
1776
|
+
this.msg_count.reject++
|
|
1777
|
+
this.transaction.msg_status = 'rejected'
|
|
1778
|
+
this.reset_transaction(() => {
|
|
1779
|
+
this.resume()
|
|
1780
|
+
})
|
|
1781
|
+
})
|
|
1782
|
+
}
|
|
1783
|
+
})
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
queue_respond(retval, msg) {
|
|
1787
|
+
msg = this.queue_msg(retval, msg)
|
|
1788
|
+
this.store_queue_result(retval, msg)
|
|
1789
|
+
msg = `${msg} (${this.transaction.uuid})`
|
|
1790
|
+
|
|
1791
|
+
if (retval !== constants.ok) {
|
|
1792
|
+
this.lognotice('queue', {
|
|
1793
|
+
code: constants.translate(retval),
|
|
1794
|
+
msg,
|
|
1795
|
+
})
|
|
1796
|
+
}
|
|
1797
|
+
switch (retval) {
|
|
1798
|
+
case constants.ok:
|
|
1799
|
+
plugins.run_hooks('queue_ok', this, msg)
|
|
1800
|
+
break
|
|
1801
|
+
case constants.deny:
|
|
1802
|
+
this.respond(550, msg, () => {
|
|
1803
|
+
this.msg_count.reject++
|
|
1804
|
+
this.transaction.msg_status = 'rejected'
|
|
1805
|
+
this.reset_transaction(() => this.resume())
|
|
1806
|
+
})
|
|
1807
|
+
break
|
|
1808
|
+
case constants.denydisconnect:
|
|
1809
|
+
this.respond(550, msg, () => {
|
|
1810
|
+
this.msg_count.reject++
|
|
1811
|
+
this.transaction.msg_status = 'rejected'
|
|
1812
|
+
this.disconnect()
|
|
1813
|
+
})
|
|
1814
|
+
break
|
|
1815
|
+
case constants.denysoft:
|
|
1816
|
+
this.respond(450, msg, () => {
|
|
1817
|
+
this.msg_count.tempfail++
|
|
1818
|
+
this.transaction.msg_status = 'deferred'
|
|
1819
|
+
this.reset_transaction(() => this.resume())
|
|
1820
|
+
})
|
|
1821
|
+
break
|
|
1822
|
+
case constants.denysoftdisconnect:
|
|
1823
|
+
this.respond(450, msg, () => {
|
|
1824
|
+
this.msg_count.tempfail++
|
|
1825
|
+
this.transaction.msg_status = 'deferred'
|
|
1826
|
+
this.disconnect()
|
|
1827
|
+
})
|
|
1828
|
+
break
|
|
1829
|
+
default:
|
|
1830
|
+
if (!msg) msg = 'Queuing declined or disabled, try later'
|
|
1831
|
+
this.respond(451, msg, () => {
|
|
1832
|
+
this.msg_count.tempfail++
|
|
1833
|
+
this.transaction.msg_status = 'deferred'
|
|
1834
|
+
this.reset_transaction(() => this.resume())
|
|
1835
|
+
})
|
|
1836
|
+
break
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
queue_ok_respond(retval, msg, params) {
|
|
1840
|
+
// This hook is common to both hook_queue and hook_queue_outbound
|
|
1841
|
+
// retval and msg are ignored in this hook so we always log OK
|
|
1842
|
+
this.lognotice('queue', {
|
|
1843
|
+
code: 'OK',
|
|
1844
|
+
msg: params || '',
|
|
1845
|
+
})
|
|
1846
|
+
|
|
1847
|
+
this.respond(250, params, () => {
|
|
1848
|
+
this.msg_count.accept++
|
|
1849
|
+
if (this.transaction) this.transaction.msg_status = 'accepted'
|
|
1850
|
+
this.reset_transaction(() => this.resume())
|
|
1851
|
+
})
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
exports.Connection = Connection
|
|
1856
|
+
|
|
1857
|
+
exports.cfg = cfg
|
|
1858
|
+
|
|
1859
|
+
exports.createConnection = (client, server, cfg) => {
|
|
1860
|
+
return new Connection(client, server, cfg)
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
logger.add_log_methods(Connection)
|