Haraka 3.1.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +4 -0
- package/CONTRIBUTORS.md +5 -5
- package/Changes.md +69 -50
- package/Plugins.md +3 -1
- package/README.md +1 -1
- package/bin/haraka +475 -478
- package/config/outbound.ini +3 -0
- package/connection.js +1072 -1108
- package/docs/Connection.md +29 -30
- package/docs/CoreConfig.md +38 -39
- package/docs/CustomReturnCodes.md +0 -1
- package/docs/HAProxy.md +2 -2
- package/docs/Header.md +1 -1
- package/docs/Logging.md +29 -5
- package/docs/Outbound.md +93 -78
- package/docs/Plugins.md +103 -108
- package/docs/Transaction.md +49 -51
- package/docs/Tutorial.md +127 -143
- package/docs/deprecated/access.md +0 -1
- package/docs/deprecated/backscatterer.md +2 -3
- package/docs/deprecated/connect.rdns_access.md +18 -27
- package/docs/deprecated/data.headers.md +0 -1
- package/docs/deprecated/data.nomsgid.md +1 -2
- package/docs/deprecated/data.noreceived.md +1 -2
- package/docs/deprecated/data.rfc5322_header_checks.md +1 -2
- package/docs/deprecated/dkim_sign.md +13 -17
- package/docs/deprecated/dkim_verify.md +9 -17
- package/docs/deprecated/dnsbl.md +36 -38
- package/docs/deprecated/dnswl.md +41 -43
- package/docs/deprecated/lookup_rdns.strict.md +21 -34
- package/docs/deprecated/mail_from.access.md +17 -25
- package/docs/deprecated/mail_from.blocklist.md +9 -12
- package/docs/deprecated/mail_from.nobounces.md +1 -2
- package/docs/deprecated/rcpt_to.access.md +20 -27
- package/docs/deprecated/rcpt_to.blocklist.md +10 -13
- package/docs/deprecated/rcpt_to.routes.md +0 -1
- package/docs/deprecated/rdns.regexp.md +13 -15
- package/docs/plugins/aliases.md +89 -89
- package/docs/plugins/auth/auth_bridge.md +5 -7
- package/docs/plugins/auth/auth_ldap.md +11 -14
- package/docs/plugins/auth/auth_proxy.md +10 -12
- package/docs/plugins/auth/auth_vpopmaild.md +5 -6
- package/docs/plugins/auth/flat_file.md +4 -4
- package/docs/plugins/block_me.md +3 -3
- package/docs/plugins/data.signatures.md +1 -2
- package/docs/plugins/delay_deny.md +3 -4
- package/docs/plugins/max_unrecognized_commands.md +4 -4
- package/docs/plugins/prevent_credential_leaks.md +6 -6
- package/docs/plugins/process_title.md +18 -18
- package/docs/plugins/queue/deliver.md +2 -3
- package/docs/plugins/queue/discard.md +4 -4
- package/docs/plugins/queue/lmtp.md +1 -3
- package/docs/plugins/queue/qmail-queue.md +7 -9
- package/docs/plugins/queue/quarantine.md +16 -21
- package/docs/plugins/queue/rabbitmq.md +8 -11
- package/docs/plugins/queue/rabbitmq_amqplib.md +43 -39
- package/docs/plugins/queue/smtp_bridge.md +7 -10
- package/docs/plugins/queue/smtp_forward.md +42 -34
- package/docs/plugins/queue/smtp_proxy.md +30 -29
- package/docs/plugins/queue/test.md +1 -3
- package/docs/plugins/rcpt_to.in_host_list.md +6 -6
- package/docs/plugins/rcpt_to.max_count.md +1 -1
- package/docs/plugins/record_envelope_addresses.md +3 -3
- package/docs/plugins/reseed_rng.md +6 -6
- package/docs/plugins/status.md +9 -8
- package/docs/plugins/tarpit.md +7 -11
- package/docs/plugins/tls.md +12 -17
- package/docs/plugins/toobusy.md +4 -4
- package/docs/plugins/xclient.md +3 -3
- package/docs/tutorials/Migrating_from_v1_to_v2.md +19 -41
- package/docs/tutorials/SettingUpOutbound.md +6 -9
- package/endpoint.js +35 -38
- package/eslint.config.mjs +22 -19
- package/haraka.js +42 -47
- package/host_pool.js +75 -79
- package/http/html/404.html +45 -49
- package/http/html/index.html +39 -28
- package/http/package.json +2 -4
- package/line_socket.js +27 -28
- package/logger.js +182 -201
- package/outbound/client_pool.js +34 -27
- package/outbound/config.js +64 -59
- package/outbound/fsync_writestream.js +24 -25
- package/outbound/hmail.js +888 -835
- package/outbound/index.js +194 -187
- package/outbound/qfile.js +49 -52
- package/outbound/queue.js +197 -190
- package/outbound/timer_queue.js +41 -43
- package/outbound/tls.js +68 -61
- package/outbound/todo.js +11 -11
- package/package.json +38 -33
- package/plugins/.eslintrc.yaml +0 -1
- package/plugins/auth/auth_base.js +123 -127
- package/plugins/auth/auth_bridge.js +7 -7
- package/plugins/auth/auth_proxy.js +121 -126
- package/plugins/auth/auth_vpopmaild.js +84 -85
- package/plugins/auth/flat_file.js +18 -17
- package/plugins/block_me.js +31 -31
- package/plugins/data.signatures.js +13 -13
- package/plugins/delay_deny.js +65 -61
- package/plugins/prevent_credential_leaks.js +23 -23
- package/plugins/process_title.js +125 -128
- package/plugins/profile.js +5 -5
- package/plugins/queue/deliver.js +3 -3
- package/plugins/queue/discard.js +13 -14
- package/plugins/queue/lmtp.js +16 -17
- package/plugins/queue/qmail-queue.js +54 -55
- package/plugins/queue/quarantine.js +68 -70
- package/plugins/queue/rabbitmq.js +80 -87
- package/plugins/queue/rabbitmq_amqplib.js +75 -54
- package/plugins/queue/smtp_bridge.js +16 -16
- package/plugins/queue/smtp_forward.js +175 -179
- package/plugins/queue/smtp_proxy.js +69 -71
- package/plugins/queue/test.js +9 -9
- package/plugins/rcpt_to.host_list_base.js +30 -34
- package/plugins/rcpt_to.in_host_list.js +19 -19
- package/plugins/record_envelope_addresses.js +4 -4
- package/plugins/reseed_rng.js +4 -4
- package/plugins/status.js +90 -97
- package/plugins/tarpit.js +25 -14
- package/plugins/tls.js +68 -68
- package/plugins/toobusy.js +21 -23
- package/plugins/xclient.js +51 -53
- package/plugins.js +276 -293
- package/rfc1869.js +30 -35
- package/server.js +308 -299
- package/smtp_client.js +244 -228
- package/test/.eslintrc.yaml +0 -1
- package/test/connection.js +127 -134
- package/test/endpoint.js +53 -47
- package/test/fixtures/line_socket.js +12 -12
- package/test/fixtures/util_hmailitem.js +89 -85
- package/test/host_pool.js +90 -92
- package/test/installation/plugins/base_plugin.js +2 -2
- package/test/installation/plugins/folder_plugin/index.js +2 -3
- package/test/installation/plugins/inherits.js +3 -3
- package/test/installation/plugins/load_first.js +2 -3
- package/test/installation/plugins/plugin.js +1 -3
- package/test/installation/plugins/tls.js +2 -4
- package/test/logger.js +135 -116
- package/test/outbound/hmail.js +49 -35
- package/test/outbound/index.js +118 -101
- package/test/outbound/qfile.js +51 -53
- package/test/outbound_bounce_net_errors.js +84 -69
- package/test/outbound_bounce_rfc3464.js +235 -165
- package/test/plugins/auth/auth_base.js +420 -279
- package/test/plugins/auth/auth_vpopmaild.js +38 -39
- package/test/plugins/queue/smtp_forward.js +126 -104
- package/test/plugins/rcpt_to.host_list_base.js +85 -67
- package/test/plugins/rcpt_to.in_host_list.js +159 -112
- package/test/plugins/status.js +71 -64
- package/test/plugins/tls.js +37 -34
- package/test/plugins.js +97 -92
- package/test/rfc1869.js +19 -26
- package/test/server.js +293 -272
- package/test/smtp_client.js +180 -176
- package/test/tls_socket.js +62 -66
- package/test/transaction.js +159 -160
- package/tls_socket.js +331 -333
- package/transaction.js +129 -137
package/smtp_client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
'use strict'
|
|
1
|
+
'use strict'
|
|
2
2
|
// SMTP client object and class. This allows every part of the client
|
|
3
3
|
// protocol to be hooked for different levels of control, such as
|
|
4
4
|
// smtp_forward and smtp_proxy queue plugins.
|
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
// than a bunch of connections to a single host from the configuration values
|
|
8
8
|
// in "host" and "port" (see host_pool.js).
|
|
9
9
|
|
|
10
|
-
const events = require('node:events')
|
|
10
|
+
const events = require('node:events')
|
|
11
11
|
|
|
12
|
-
const ipaddr
|
|
13
|
-
const net_utils = require('haraka-net-utils')
|
|
14
|
-
const utils
|
|
12
|
+
const ipaddr = require('ipaddr.js')
|
|
13
|
+
const net_utils = require('haraka-net-utils')
|
|
14
|
+
const utils = require('haraka-utils')
|
|
15
15
|
|
|
16
16
|
const tls_socket = require('./tls_socket')
|
|
17
|
-
const logger
|
|
18
|
-
const HostPool
|
|
17
|
+
const logger = require('./logger')
|
|
18
|
+
const HostPool = require('./host_pool')
|
|
19
19
|
|
|
20
|
-
const smtp_regexp = /^(\d{3})([ -])(.*)
|
|
20
|
+
const smtp_regexp = /^(\d{3})([ -])(.*)/
|
|
21
21
|
const STATE = {
|
|
22
22
|
IDLE: 1,
|
|
23
23
|
ACTIVE: 2,
|
|
@@ -26,109 +26,110 @@ const STATE = {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
class SMTPClient extends events.EventEmitter {
|
|
29
|
-
constructor
|
|
30
|
-
super()
|
|
31
|
-
this.uuid = utils.uuid()
|
|
32
|
-
this.connect_timeout = parseInt(opts.connect_timeout) || 30
|
|
29
|
+
constructor(opts = {}) {
|
|
30
|
+
super()
|
|
31
|
+
this.uuid = utils.uuid()
|
|
32
|
+
this.connect_timeout = parseInt(opts.connect_timeout) || 30
|
|
33
33
|
this.socket = opts.socket || this.get_socket(opts)
|
|
34
|
-
this.socket.setTimeout(this.connect_timeout * 1000)
|
|
35
|
-
this.socket.setKeepAlive(true)
|
|
36
|
-
this.state = STATE.IDLE
|
|
37
|
-
this.command = 'greeting'
|
|
38
|
-
this.response = []
|
|
39
|
-
this.connected = false
|
|
40
|
-
this.authenticating= false
|
|
41
|
-
this.authenticated = false
|
|
42
|
-
this.auth_capabilities = []
|
|
43
|
-
this.host = opts.host
|
|
44
|
-
this.port = opts.port
|
|
45
|
-
this.smtputf8 = false
|
|
46
|
-
|
|
47
|
-
const client = this
|
|
34
|
+
this.socket.setTimeout(this.connect_timeout * 1000)
|
|
35
|
+
this.socket.setKeepAlive(true)
|
|
36
|
+
this.state = STATE.IDLE
|
|
37
|
+
this.command = 'greeting'
|
|
38
|
+
this.response = []
|
|
39
|
+
this.connected = false
|
|
40
|
+
this.authenticating = false
|
|
41
|
+
this.authenticated = false
|
|
42
|
+
this.auth_capabilities = []
|
|
43
|
+
this.host = opts.host
|
|
44
|
+
this.port = opts.port
|
|
45
|
+
this.smtputf8 = false
|
|
46
|
+
|
|
47
|
+
const client = this
|
|
48
48
|
|
|
49
49
|
client.socket.on('line', (line) => {
|
|
50
|
-
client.emit('server_protocol', line)
|
|
51
|
-
const matches = smtp_regexp.exec(line)
|
|
50
|
+
client.emit('server_protocol', line)
|
|
51
|
+
const matches = smtp_regexp.exec(line)
|
|
52
52
|
if (!matches) {
|
|
53
|
-
client.emit('error', `${client.uuid}: Unrecognized response from upstream server: ${line}`)
|
|
54
|
-
client.destroy()
|
|
55
|
-
return
|
|
53
|
+
client.emit('error', `${client.uuid}: Unrecognized response from upstream server: ${line}`)
|
|
54
|
+
client.destroy()
|
|
55
|
+
return
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const code = matches[1]
|
|
59
|
-
const cont = matches[2]
|
|
60
|
-
const msg = matches[3]
|
|
58
|
+
const code = matches[1]
|
|
59
|
+
const cont = matches[2]
|
|
60
|
+
const msg = matches[3]
|
|
61
61
|
|
|
62
|
-
client.response.push(msg)
|
|
63
|
-
if (cont !== ' ') return
|
|
62
|
+
client.response.push(msg)
|
|
63
|
+
if (cont !== ' ') return
|
|
64
64
|
|
|
65
65
|
if (client.command === 'auth' || client.authenticating) {
|
|
66
|
-
logger.info(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
logger.info(
|
|
67
|
+
`SERVER RESPONSE, CLIENT ${client.command}, authenticating=${client.authenticating},code=${code},cont=${cont},msg=${msg}`,
|
|
68
|
+
)
|
|
69
|
+
if (
|
|
70
|
+
/^3/.test(code) &&
|
|
71
|
+
(msg === 'VXNlcm5hbWU6' || msg === 'dXNlcm5hbWU6') // Workaround ill-mannered SMTP servers (namely smtp.163.com)
|
|
72
|
+
) {
|
|
73
|
+
client.emit('auth_username')
|
|
74
|
+
return
|
|
73
75
|
}
|
|
74
76
|
if (/^3/.test(code) && msg === 'UGFzc3dvcmQ6') {
|
|
75
|
-
client.emit('auth_password')
|
|
76
|
-
return
|
|
77
|
+
client.emit('auth_password')
|
|
78
|
+
return
|
|
77
79
|
}
|
|
78
80
|
if (/^2/.test(code) && client.authenticating) {
|
|
79
|
-
logger.info('AUTHENTICATED')
|
|
80
|
-
client.authenticating = false
|
|
81
|
-
client.authenticated = true
|
|
82
|
-
client.emit('auth')
|
|
83
|
-
return
|
|
81
|
+
logger.info('AUTHENTICATED')
|
|
82
|
+
client.authenticating = false
|
|
83
|
+
client.authenticated = true
|
|
84
|
+
client.emit('auth')
|
|
85
|
+
return
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
if (client.command === 'ehlo') {
|
|
88
90
|
if (code.match(/^5/)) {
|
|
89
91
|
// Handle fallback to HELO if EHLO is rejected
|
|
90
|
-
client.emit('greeting', 'HELO')
|
|
91
|
-
return
|
|
92
|
+
client.emit('greeting', 'HELO')
|
|
93
|
+
return
|
|
92
94
|
}
|
|
93
|
-
client.emit('capabilities')
|
|
95
|
+
client.emit('capabilities')
|
|
94
96
|
if (client.command !== 'ehlo') {
|
|
95
|
-
return
|
|
97
|
+
return
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
if (client.command === 'xclient' && /^5/.test(code)) {
|
|
100
102
|
// XCLIENT command was rejected (no permission?)
|
|
101
103
|
// Carry on without XCLIENT
|
|
102
|
-
client.command = 'helo'
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
client.emit('bad_code', code, client.response.join(' '));
|
|
104
|
+
client.command = 'helo'
|
|
105
|
+
} else if (/^[45]/.test(code)) {
|
|
106
|
+
client.emit('bad_code', code, client.response.join(' '))
|
|
106
107
|
if (client.state !== STATE.ACTIVE) {
|
|
107
|
-
return
|
|
108
|
+
return
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
if (/^441/.test(code)) {
|
|
112
113
|
if (/Connection timed out/i.test(msg)) {
|
|
113
|
-
client.destroy()
|
|
114
|
+
client.destroy()
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
switch (client.command) {
|
|
118
119
|
case 'xclient':
|
|
119
|
-
client.xclient = true
|
|
120
|
-
client.emit('xclient', 'EHLO')
|
|
121
|
-
break
|
|
120
|
+
client.xclient = true
|
|
121
|
+
client.emit('xclient', 'EHLO')
|
|
122
|
+
break
|
|
122
123
|
case 'starttls':
|
|
123
|
-
client.upgrade(client.tls_options)
|
|
124
|
-
break
|
|
124
|
+
client.upgrade(client.tls_options)
|
|
125
|
+
break
|
|
125
126
|
case 'greeting':
|
|
126
|
-
client.connected = true
|
|
127
|
-
client.emit('greeting', 'EHLO')
|
|
128
|
-
break
|
|
127
|
+
client.connected = true
|
|
128
|
+
client.emit('greeting', 'EHLO')
|
|
129
|
+
break
|
|
129
130
|
case 'ehlo':
|
|
130
|
-
client.emit('helo')
|
|
131
|
-
break
|
|
131
|
+
client.emit('helo')
|
|
132
|
+
break
|
|
132
133
|
case 'helo':
|
|
133
134
|
case 'mail':
|
|
134
135
|
case 'rcpt':
|
|
@@ -136,129 +137,144 @@ class SMTPClient extends events.EventEmitter {
|
|
|
136
137
|
case 'dot':
|
|
137
138
|
case 'rset':
|
|
138
139
|
case 'auth':
|
|
139
|
-
client.emit(client.command)
|
|
140
|
-
break
|
|
140
|
+
client.emit(client.command)
|
|
141
|
+
break
|
|
141
142
|
case 'quit':
|
|
142
|
-
client.emit('quit')
|
|
143
|
-
client.destroy()
|
|
144
|
-
break
|
|
143
|
+
client.emit('quit')
|
|
144
|
+
client.destroy()
|
|
145
|
+
break
|
|
145
146
|
default:
|
|
146
|
-
throw new Error(`Unknown command: ${client.command}`)
|
|
147
|
+
throw new Error(`Unknown command: ${client.command}`)
|
|
147
148
|
}
|
|
148
|
-
})
|
|
149
|
+
})
|
|
149
150
|
|
|
150
151
|
client.socket.on('connect', () => {
|
|
151
152
|
// Replace connection timeout with idle timeout
|
|
152
|
-
client.socket.setTimeout((opts.idle_timeout || 300) * 1000)
|
|
153
|
+
client.socket.setTimeout((opts.idle_timeout || 300) * 1000)
|
|
153
154
|
if (!client.socket.remoteAddress) {
|
|
154
155
|
// "Value may be undefined if the socket is destroyed"
|
|
155
|
-
logger.debug('socket.remoteAddress undefined')
|
|
156
|
-
return
|
|
156
|
+
logger.debug('socket.remoteAddress undefined')
|
|
157
|
+
return
|
|
157
158
|
}
|
|
158
|
-
client.remote_ip = ipaddr.process(client.socket.remoteAddress).toString()
|
|
159
|
+
client.remote_ip = ipaddr.process(client.socket.remoteAddress).toString()
|
|
159
160
|
})
|
|
160
161
|
|
|
161
|
-
function closed
|
|
162
|
-
return error => {
|
|
163
|
-
if (!error) error = ''
|
|
162
|
+
function closed(msg) {
|
|
163
|
+
return (error) => {
|
|
164
|
+
if (!error) error = ''
|
|
164
165
|
|
|
165
166
|
// error is e.g. "Error: connect ECONNREFUSED"
|
|
166
|
-
const errMsg = `${client.uuid}: [${client.host}:${client.port}] SMTP connection ${msg} ${error}
|
|
167
|
+
const errMsg = `${client.uuid}: [${client.host}:${client.port}] SMTP connection ${msg} ${error}`
|
|
167
168
|
|
|
168
169
|
/* eslint-disable no-fallthrough */
|
|
169
170
|
switch (client.state) {
|
|
170
171
|
case STATE.ACTIVE:
|
|
171
|
-
client.emit('error', errMsg)
|
|
172
|
+
client.emit('error', errMsg)
|
|
172
173
|
case STATE.IDLE:
|
|
173
174
|
case STATE.RELEASED:
|
|
174
|
-
client.destroy()
|
|
175
|
-
break
|
|
175
|
+
client.destroy()
|
|
176
|
+
break
|
|
176
177
|
case STATE.DESTROYED:
|
|
177
178
|
if (msg === 'errored' || msg === 'timed out') {
|
|
178
|
-
client.emit('connection-error', errMsg)
|
|
179
|
+
client.emit('connection-error', errMsg)
|
|
179
180
|
}
|
|
180
181
|
break
|
|
181
182
|
default:
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
logger.debug(`[smtp_client] ${errMsg} (state=${client.state})`)
|
|
185
|
+
logger.debug(`[smtp_client] ${errMsg} (state=${client.state})`)
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
client.socket.on('error',
|
|
189
|
-
client.socket.on('timeout', closed('timed out'))
|
|
190
|
-
client.socket.on('close',
|
|
191
|
-
client.socket.on('end',
|
|
189
|
+
client.socket.on('error', closed('errored'))
|
|
190
|
+
client.socket.on('timeout', closed('timed out'))
|
|
191
|
+
client.socket.on('close', closed('closed'))
|
|
192
|
+
client.socket.on('end', closed('ended'))
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
load_tls_config
|
|
195
|
-
|
|
196
|
-
const tls_options = { servername: this.host };
|
|
195
|
+
load_tls_config(opts) {
|
|
196
|
+
const tls_options = { servername: this.host }
|
|
197
197
|
if (opts) {
|
|
198
|
-
Object.assign(tls_options, opts)
|
|
198
|
+
Object.assign(tls_options, opts)
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
this.tls_options = tls_options
|
|
201
|
+
this.tls_options = tls_options
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
send_command
|
|
205
|
-
const line =
|
|
206
|
-
this.emit('client_protocol', line)
|
|
207
|
-
this.command = command.toLowerCase()
|
|
208
|
-
this.response = []
|
|
209
|
-
this.socket.write(`${line}\r\n`)
|
|
204
|
+
send_command(command, data) {
|
|
205
|
+
const line = command === 'dot' ? '.' : command + (data ? ` ${data}` : '')
|
|
206
|
+
this.emit('client_protocol', line)
|
|
207
|
+
this.command = command.toLowerCase()
|
|
208
|
+
this.response = []
|
|
209
|
+
this.socket.write(`${line}\r\n`)
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
start_data
|
|
213
|
-
this.response = []
|
|
214
|
-
this.command = 'dot'
|
|
215
|
-
|
|
212
|
+
start_data(data) {
|
|
213
|
+
this.response = []
|
|
214
|
+
this.command = 'dot'
|
|
215
|
+
// SUNSET: dot_stuffing was renamed to dot_stuffed, remove it after 2026-01
|
|
216
|
+
data.pipe(this.socket, {
|
|
217
|
+
dot_stuffed: false,
|
|
218
|
+
dot_stuffing: true,
|
|
219
|
+
ending_dot: true,
|
|
220
|
+
end: false,
|
|
221
|
+
})
|
|
216
222
|
}
|
|
217
223
|
|
|
218
|
-
release
|
|
219
|
-
if (this.state === STATE.DESTROYED) return
|
|
220
|
-
logger.debug(`[smtp_client] ${this.uuid} releasing, state=${this.state}`)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
'
|
|
224
|
-
'
|
|
225
|
-
'
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
release() {
|
|
225
|
+
if (this.state === STATE.DESTROYED) return
|
|
226
|
+
logger.debug(`[smtp_client] ${this.uuid} releasing, state=${this.state}`)
|
|
227
|
+
;[
|
|
228
|
+
'auth',
|
|
229
|
+
'bad_code',
|
|
230
|
+
'capabilities',
|
|
231
|
+
'client_protocol',
|
|
232
|
+
'connection-error',
|
|
233
|
+
'data',
|
|
234
|
+
'dot',
|
|
235
|
+
'error',
|
|
236
|
+
'greeting',
|
|
237
|
+
'helo',
|
|
238
|
+
'mail',
|
|
239
|
+
'rcpt',
|
|
240
|
+
'rset',
|
|
241
|
+
'server_protocol',
|
|
242
|
+
'xclient',
|
|
243
|
+
].forEach((l) => {
|
|
244
|
+
this.removeAllListeners(l)
|
|
228
245
|
})
|
|
229
246
|
|
|
230
|
-
if (this.connected) this.send_command('QUIT')
|
|
247
|
+
if (this.connected) this.send_command('QUIT')
|
|
231
248
|
this.destroy()
|
|
232
249
|
}
|
|
233
250
|
|
|
234
|
-
destroy
|
|
251
|
+
destroy() {
|
|
235
252
|
if (this.state === STATE.DESTROYED) return
|
|
236
|
-
this.state = STATE.DESTROYED
|
|
237
|
-
this.socket.destroy()
|
|
253
|
+
this.state = STATE.DESTROYED
|
|
254
|
+
this.socket.destroy()
|
|
238
255
|
}
|
|
239
256
|
|
|
240
|
-
upgrade
|
|
241
|
-
|
|
257
|
+
upgrade(tls_options) {
|
|
242
258
|
this.socket.upgrade(tls_options, (verified, verifyError, cert, cipher) => {
|
|
243
|
-
logger.info(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
})
|
|
259
|
+
logger.info(
|
|
260
|
+
`secured:${
|
|
261
|
+
cipher ? ` cipher=${cipher.name} version=${cipher.version}` : ''
|
|
262
|
+
} verified=${verified}${verifyError ? ` error="${verifyError}"` : ''}${
|
|
263
|
+
cert?.subject ? ` cn="${cert.subject.CN}" organization="${cert.subject.O}"` : ''
|
|
264
|
+
}${cert?.issuer ? ` issuer="${cert.issuer.O}"` : ''}${
|
|
265
|
+
cert?.valid_to ? ` expires="${cert.valid_to}"` : ''
|
|
266
|
+
}${cert?.fingerprint ? ` fingerprint=${cert.fingerprint}` : ''}`,
|
|
267
|
+
)
|
|
268
|
+
})
|
|
253
269
|
}
|
|
254
270
|
|
|
255
|
-
is_dead_sender
|
|
256
|
-
if (connection?.transaction) return false
|
|
271
|
+
is_dead_sender(plugin, connection) {
|
|
272
|
+
if (connection?.transaction) return false
|
|
257
273
|
|
|
258
274
|
// This likely means the sender went away on us, cleanup.
|
|
259
|
-
connection.logwarn(plugin,
|
|
260
|
-
this.release()
|
|
261
|
-
return true
|
|
275
|
+
connection.logwarn(plugin, 'transaction went away, releasing smtp_client')
|
|
276
|
+
this.release()
|
|
277
|
+
return true
|
|
262
278
|
}
|
|
263
279
|
|
|
264
280
|
get_socket(opts) {
|
|
@@ -272,7 +288,7 @@ class SMTPClient extends events.EventEmitter {
|
|
|
272
288
|
}
|
|
273
289
|
}
|
|
274
290
|
|
|
275
|
-
exports.smtp_client = SMTPClient
|
|
291
|
+
exports.smtp_client = SMTPClient
|
|
276
292
|
|
|
277
293
|
// Get a smtp_client for the given attributes.
|
|
278
294
|
// used only in testing
|
|
@@ -286,41 +302,40 @@ exports.onCapabilitiesOutbound = (smtp_client, secured, connection, config, on_s
|
|
|
286
302
|
for (const line in smtp_client.response) {
|
|
287
303
|
if (/^XCLIENT/.test(smtp_client.response[line])) {
|
|
288
304
|
if (!smtp_client.xclient) {
|
|
289
|
-
smtp_client.send_command('XCLIENT', `ADDR=${connection.remote.ip}`)
|
|
290
|
-
return
|
|
305
|
+
smtp_client.send_command('XCLIENT', `ADDR=${connection.remote.ip}`)
|
|
306
|
+
return
|
|
291
307
|
}
|
|
292
308
|
}
|
|
293
309
|
|
|
294
310
|
if (/^SMTPUTF8/.test(smtp_client.response[line])) {
|
|
295
|
-
smtp_client.smtputf8 = true
|
|
311
|
+
smtp_client.smtputf8 = true
|
|
296
312
|
}
|
|
297
313
|
|
|
298
314
|
if (/^STARTTLS/.test(smtp_client.response[line]) && !secured) {
|
|
299
|
-
|
|
300
315
|
let hostBanned = false
|
|
301
316
|
let serverBanned = false
|
|
302
317
|
|
|
303
318
|
// Check if there are any banned TLS hosts
|
|
304
319
|
if (smtp_client.tls_options.no_tls_hosts) {
|
|
305
320
|
// If there are check if these hosts are in the blacklist
|
|
306
|
-
hostBanned = net_utils.ip_in_list(smtp_client.tls_config.no_tls_hosts, config.host)
|
|
307
|
-
serverBanned = net_utils.ip_in_list(smtp_client.tls_config.no_tls_hosts, smtp_client.remote_ip)
|
|
321
|
+
hostBanned = net_utils.ip_in_list(smtp_client.tls_config.no_tls_hosts, config.host)
|
|
322
|
+
serverBanned = net_utils.ip_in_list(smtp_client.tls_config.no_tls_hosts, smtp_client.remote_ip)
|
|
308
323
|
}
|
|
309
324
|
|
|
310
325
|
if (!hostBanned && !serverBanned && config.enable_tls) {
|
|
311
|
-
smtp_client.socket.on('secure', on_secured)
|
|
312
|
-
smtp_client.secured = false
|
|
313
|
-
smtp_client.send_command('STARTTLS')
|
|
314
|
-
return
|
|
326
|
+
smtp_client.socket.on('secure', on_secured)
|
|
327
|
+
smtp_client.secured = false // have to wait in forward plugin before we can do auth, even if capabilities are there on first EHLO
|
|
328
|
+
smtp_client.send_command('STARTTLS')
|
|
329
|
+
return
|
|
315
330
|
}
|
|
316
331
|
}
|
|
317
332
|
|
|
318
|
-
let auth_matches = smtp_client.response[line].match(/^AUTH (.*)$/)
|
|
333
|
+
let auth_matches = smtp_client.response[line].match(/^AUTH (.*)$/)
|
|
319
334
|
if (auth_matches) {
|
|
320
|
-
smtp_client.auth_capabilities = []
|
|
321
|
-
auth_matches = auth_matches[1].split(' ')
|
|
335
|
+
smtp_client.auth_capabilities = []
|
|
336
|
+
auth_matches = auth_matches[1].split(' ')
|
|
322
337
|
for (const authMatch of auth_matches) {
|
|
323
|
-
smtp_client.auth_capabilities.push(authMatch.toLowerCase())
|
|
338
|
+
smtp_client.auth_capabilities.push(authMatch.toLowerCase())
|
|
324
339
|
}
|
|
325
340
|
}
|
|
326
341
|
}
|
|
@@ -337,147 +352,148 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
337
352
|
c.auth = {
|
|
338
353
|
type: c.auth_type,
|
|
339
354
|
user: c.auth_user,
|
|
340
|
-
pass: c.auth_pass
|
|
355
|
+
pass: c.auth_pass,
|
|
341
356
|
}
|
|
342
357
|
}
|
|
343
358
|
|
|
344
|
-
const hostport = get_hostport(connection, connection.server, c)
|
|
359
|
+
const hostport = get_hostport(connection, connection.server, c)
|
|
345
360
|
const smtp_client = new SMTPClient(hostport)
|
|
346
|
-
logger.info(`[smtp_client] uuid=${smtp_client.uuid} host=${hostport.host} port=${hostport.port} created`)
|
|
361
|
+
logger.info(`[smtp_client] uuid=${smtp_client.uuid} host=${hostport.host} port=${hostport.port} created`)
|
|
347
362
|
|
|
348
|
-
connection.logdebug(plugin, `Got smtp_client: ${smtp_client.uuid}`)
|
|
363
|
+
connection.logdebug(plugin, `Got smtp_client: ${smtp_client.uuid}`)
|
|
349
364
|
|
|
350
|
-
let secured = false
|
|
365
|
+
let secured = false
|
|
351
366
|
|
|
352
|
-
smtp_client.load_tls_config(plugin.tls_options)
|
|
367
|
+
smtp_client.load_tls_config(plugin.tls_options)
|
|
353
368
|
|
|
354
369
|
smtp_client.call_next = function (retval, msg) {
|
|
355
370
|
if (this.next) {
|
|
356
|
-
const { next } = this
|
|
357
|
-
delete this.next
|
|
358
|
-
next(retval, msg)
|
|
371
|
+
const { next } = this
|
|
372
|
+
delete this.next
|
|
373
|
+
next(retval, msg)
|
|
359
374
|
}
|
|
360
375
|
}
|
|
361
376
|
|
|
362
377
|
smtp_client.on('client_protocol', (line) => {
|
|
363
|
-
connection.logprotocol(plugin, `C: ${line}`)
|
|
378
|
+
connection.logprotocol(plugin, `C: ${line}`)
|
|
364
379
|
})
|
|
365
380
|
|
|
366
381
|
smtp_client.on('server_protocol', (line) => {
|
|
367
|
-
connection.logprotocol(plugin, `S: ${line}`)
|
|
382
|
+
connection.logprotocol(plugin, `S: ${line}`)
|
|
368
383
|
})
|
|
369
384
|
|
|
370
|
-
function helo
|
|
385
|
+
function helo(command) {
|
|
371
386
|
if (smtp_client.xclient) {
|
|
372
|
-
smtp_client.send_command(command, connection.hello.host)
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
smtp_client.send_command(command, connection.local.host);
|
|
387
|
+
smtp_client.send_command(command, connection.hello.host)
|
|
388
|
+
} else {
|
|
389
|
+
smtp_client.send_command(command, connection.local.host)
|
|
376
390
|
}
|
|
377
391
|
}
|
|
378
|
-
smtp_client.on('greeting', helo)
|
|
379
|
-
smtp_client.on('xclient', helo)
|
|
380
|
-
|
|
381
|
-
function on_secured
|
|
382
|
-
if (secured) return
|
|
383
|
-
secured = true
|
|
384
|
-
smtp_client.secured = true
|
|
385
|
-
smtp_client.emit('greeting', 'EHLO')
|
|
392
|
+
smtp_client.on('greeting', helo)
|
|
393
|
+
smtp_client.on('xclient', helo)
|
|
394
|
+
|
|
395
|
+
function on_secured() {
|
|
396
|
+
if (secured) return
|
|
397
|
+
secured = true
|
|
398
|
+
smtp_client.secured = true
|
|
399
|
+
smtp_client.emit('greeting', 'EHLO')
|
|
386
400
|
}
|
|
387
401
|
|
|
388
402
|
smtp_client.on('capabilities', () => {
|
|
389
|
-
exports.onCapabilitiesOutbound(smtp_client, secured, connection, c, on_secured)
|
|
390
|
-
})
|
|
403
|
+
exports.onCapabilitiesOutbound(smtp_client, secured, connection, c, on_secured)
|
|
404
|
+
})
|
|
391
405
|
|
|
392
406
|
smtp_client.on('helo', () => {
|
|
393
407
|
if (!c.auth || smtp_client.authenticated) {
|
|
394
|
-
if (smtp_client.is_dead_sender(plugin, connection)) return
|
|
408
|
+
if (smtp_client.is_dead_sender(plugin, connection)) return
|
|
395
409
|
|
|
396
|
-
smtp_client.send_command('MAIL', `FROM:${connection.transaction.mail_from.format(!smtp_client.smtp_utf8)}`)
|
|
397
|
-
return
|
|
410
|
+
smtp_client.send_command('MAIL', `FROM:${connection.transaction.mail_from.format(!smtp_client.smtp_utf8)}`)
|
|
411
|
+
return
|
|
398
412
|
}
|
|
399
413
|
|
|
400
|
-
if (c.auth.type === null || typeof
|
|
401
|
-
const auth_type = c.auth.type.toLowerCase()
|
|
414
|
+
if (c.auth.type === null || typeof c.auth.type === 'undefined') return // Ignore blank
|
|
415
|
+
const auth_type = c.auth.type.toLowerCase()
|
|
402
416
|
if (!smtp_client.auth_capabilities.includes(auth_type)) {
|
|
403
|
-
throw new Error(
|
|
417
|
+
throw new Error(
|
|
418
|
+
`Auth type "${auth_type}" not supported by server (supports: ${smtp_client.auth_capabilities.join(',')})`,
|
|
419
|
+
)
|
|
404
420
|
}
|
|
405
421
|
switch (auth_type) {
|
|
406
422
|
case 'plain':
|
|
407
423
|
if (!c.auth.user || !c.auth.pass) {
|
|
408
|
-
throw new Error(
|
|
424
|
+
throw new Error('Must include auth.user and auth.pass for PLAIN auth.')
|
|
409
425
|
}
|
|
410
|
-
logger.debug(`[smtp_client] uuid=${smtp_client.uuid} authenticating as "${c.auth.user}"`)
|
|
411
|
-
smtp_client.send_command(
|
|
412
|
-
|
|
426
|
+
logger.debug(`[smtp_client] uuid=${smtp_client.uuid} authenticating as "${c.auth.user}"`)
|
|
427
|
+
smtp_client.send_command(
|
|
428
|
+
'AUTH',
|
|
429
|
+
`PLAIN ${utils.base64(`${c.auth.user}\0${c.auth.user}\0${c.auth.pass}`)}`,
|
|
430
|
+
)
|
|
431
|
+
break
|
|
413
432
|
case 'cram-md5':
|
|
414
|
-
throw new Error(
|
|
433
|
+
throw new Error('Not implemented')
|
|
415
434
|
default:
|
|
416
|
-
throw new Error(`Unknown AUTH type: ${auth_type}`)
|
|
435
|
+
throw new Error(`Unknown AUTH type: ${auth_type}`)
|
|
417
436
|
}
|
|
418
|
-
})
|
|
437
|
+
})
|
|
419
438
|
|
|
420
439
|
smtp_client.on('auth', () => {
|
|
421
440
|
// if authentication has been handled by plugin(s)
|
|
422
|
-
if (smtp_client.authenticating) return
|
|
441
|
+
if (smtp_client.authenticating) return
|
|
423
442
|
|
|
424
|
-
if (smtp_client.is_dead_sender(plugin, connection)) return
|
|
443
|
+
if (smtp_client.is_dead_sender(plugin, connection)) return
|
|
425
444
|
|
|
426
|
-
smtp_client.authenticated = true
|
|
427
|
-
smtp_client.send_command('MAIL', `FROM:${connection.transaction.mail_from.format(!smtp_client.smtp_utf8)}`)
|
|
428
|
-
})
|
|
445
|
+
smtp_client.authenticated = true
|
|
446
|
+
smtp_client.send_command('MAIL', `FROM:${connection.transaction.mail_from.format(!smtp_client.smtp_utf8)}`)
|
|
447
|
+
})
|
|
429
448
|
|
|
430
449
|
// these errors only get thrown when the connection is still active
|
|
431
450
|
smtp_client.on('error', (msg) => {
|
|
432
|
-
connection.logwarn(plugin, msg)
|
|
433
|
-
smtp_client.call_next()
|
|
434
|
-
})
|
|
451
|
+
connection.logwarn(plugin, msg)
|
|
452
|
+
smtp_client.call_next()
|
|
453
|
+
})
|
|
435
454
|
|
|
436
455
|
// these are the errors thrown when the connection is dead
|
|
437
456
|
smtp_client.on('connection-error', (error) => {
|
|
438
457
|
// error contains e.g. "Error: connect ECONNREFUSE"
|
|
439
|
-
logger.error(`backend failure: ${smtp_client.host}:${smtp_client.port} - ${error}`)
|
|
440
|
-
const { host_pool } = connection.server.notes
|
|
458
|
+
logger.error(`backend failure: ${smtp_client.host}:${smtp_client.port} - ${error}`)
|
|
459
|
+
const { host_pool } = connection.server.notes
|
|
441
460
|
// only exists for if forwarding_host_pool is set in the config
|
|
442
461
|
if (host_pool) {
|
|
443
|
-
host_pool.failed(smtp_client.host, smtp_client.port)
|
|
462
|
+
host_pool.failed(smtp_client.host, smtp_client.port)
|
|
444
463
|
}
|
|
445
|
-
smtp_client.call_next()
|
|
446
|
-
})
|
|
464
|
+
smtp_client.call_next()
|
|
465
|
+
})
|
|
447
466
|
|
|
448
467
|
if (smtp_client.connected) {
|
|
449
468
|
if (smtp_client.xclient) {
|
|
450
|
-
smtp_client.send_command('XCLIENT', `ADDR=${connection.remote.ip}`)
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
smtp_client.emit('helo');
|
|
469
|
+
smtp_client.send_command('XCLIENT', `ADDR=${connection.remote.ip}`)
|
|
470
|
+
} else {
|
|
471
|
+
smtp_client.emit('helo')
|
|
454
472
|
}
|
|
455
473
|
}
|
|
456
474
|
|
|
457
|
-
callback(null, smtp_client)
|
|
475
|
+
callback(null, smtp_client)
|
|
458
476
|
}
|
|
459
477
|
|
|
460
|
-
function get_hostport
|
|
461
|
-
|
|
478
|
+
function get_hostport(connection, server, cfg) {
|
|
462
479
|
if (cfg.forwarding_host_pool) {
|
|
463
|
-
if (!
|
|
464
|
-
connection.logwarn(`creating host_pool from ${cfg.forwarding_host_pool}`)
|
|
465
|
-
server.notes.host_pool =
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
);
|
|
480
|
+
if (!server.notes.host_pool) {
|
|
481
|
+
connection.logwarn(`creating host_pool from ${cfg.forwarding_host_pool}`)
|
|
482
|
+
server.notes.host_pool = new HostPool(
|
|
483
|
+
cfg.forwarding_host_pool, // 1.2.3.4:420, 5.6.7.8:420
|
|
484
|
+
cfg.dead_forwarding_host_retry_secs,
|
|
485
|
+
)
|
|
470
486
|
}
|
|
471
487
|
|
|
472
|
-
const host = server.notes.host_pool.get_host()
|
|
473
|
-
if (host) return host
|
|
488
|
+
const host = server.notes.host_pool.get_host()
|
|
489
|
+
if (host) return host // { host: 1.2.3.4, port: 567 }
|
|
474
490
|
|
|
475
|
-
logger.error('[smtp_client] no backend hosts in pool!')
|
|
476
|
-
throw new Error(
|
|
491
|
+
logger.error('[smtp_client] no backend hosts in pool!')
|
|
492
|
+
throw new Error('no backend hosts found in pool!')
|
|
477
493
|
}
|
|
478
494
|
|
|
479
|
-
if (cfg.host && cfg.port) return { host: cfg.host, port: cfg.port }
|
|
495
|
+
if (cfg.host && cfg.port) return { host: cfg.host, port: cfg.port }
|
|
480
496
|
|
|
481
|
-
logger.warn(
|
|
482
|
-
throw new Error(
|
|
497
|
+
logger.warn('[smtp_client] forwarding_host_pool or host and port were not found in config file')
|
|
498
|
+
throw new Error('You must specify either forwarding_host_pool or host and port')
|
|
483
499
|
}
|