haraka 0.0.33 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +28 -0
- package/.githooks/pre-commit +41 -0
- package/.prettierignore +6 -0
- package/.qlty/.gitignore +7 -0
- package/.qlty/configs/.shellcheckrc +1 -0
- package/.qlty/qlty.toml +15 -0
- package/CHANGELOG.md +1894 -0
- package/CLAUDE.md +40 -0
- package/CONTRIBUTORS.md +34 -0
- package/Dockerfile +50 -0
- package/GEMINI.md +38 -0
- package/LICENSE +22 -0
- package/Plugins.md +227 -0
- package/README.md +119 -4
- package/SECURITY.md +178 -0
- package/TODO +22 -0
- package/address.js +53 -0
- package/bin/haraka +593 -0
- package/bin/haraka_grep +32 -0
- package/config/aliases +2 -0
- package/config/auth_flat_file.ini +7 -0
- package/config/auth_vpopmaild.ini +9 -0
- package/config/connection.ini +79 -0
- package/config/delay_deny.ini +7 -0
- package/config/dhparams.pem +8 -0
- package/config/host_list +3 -0
- package/config/host_list_regex +6 -0
- package/config/http.ini +11 -0
- package/config/lmtp.ini +7 -0
- package/config/log.ini +11 -0
- package/config/me +1 -0
- package/config/outbound.bounce_message +18 -0
- package/config/outbound.bounce_message_html +36 -0
- package/config/outbound.bounce_message_image +106 -0
- package/config/outbound.ini +24 -0
- package/config/plugins +67 -0
- package/config/smtp.ini +37 -0
- package/config/smtp_bridge.ini +4 -0
- package/config/smtp_forward.ini +31 -0
- package/config/smtp_proxy.ini +27 -0
- package/config/tarpit.timeout +1 -0
- package/config/tls.ini +83 -0
- package/config/tls_cert.pem +23 -0
- package/config/tls_key.pem +28 -0
- package/config/watch.ini +12 -0
- package/config/xclient.hosts +2 -0
- package/connection.js +1863 -0
- package/contrib/Haraka.cf +6 -0
- package/contrib/Haraka.pm +35 -0
- package/contrib/bad_smtp_server.pl +25 -0
- package/contrib/bsd-rc.d/haraka +61 -0
- package/contrib/debian-init.d/haraka +87 -0
- package/contrib/haraka.init +96 -0
- package/contrib/haraka.service +23 -0
- package/contrib/plugin2npm.sh +81 -0
- package/contrib/ubuntu-upstart/haraka.conf +27 -0
- package/coverage/coverage-final.json +2 -0
- package/coverage/coverage-summary.json +33 -0
- package/coverage/tmp/coverage-79131-1779241025146-0.json +1 -0
- package/coverage/tmp/coverage-79132-1779240999690-0.json +1 -0
- package/coverage/tmp/coverage-79172-1779241000095-0.json +1 -0
- package/coverage/tmp/coverage-79210-1779241000156-0.json +1 -0
- package/coverage/tmp/coverage-79211-1779241000209-0.json +1 -0
- package/coverage/tmp/coverage-79212-1779241000266-0.json +1 -0
- package/coverage/tmp/coverage-79213-1779241000441-0.json +1 -0
- package/coverage/tmp/coverage-79214-1779241000626-0.json +1 -0
- package/coverage/tmp/coverage-79215-1779241000795-0.json +1 -0
- package/coverage/tmp/coverage-79216-1779241000965-0.json +1 -0
- package/coverage/tmp/coverage-79218-1779241001013-0.json +1 -0
- package/coverage/tmp/coverage-79219-1779241001179-0.json +1 -0
- package/coverage/tmp/coverage-79220-1779241006249-0.json +1 -0
- package/coverage/tmp/coverage-79227-1779241011453-0.json +1 -0
- package/coverage/tmp/coverage-79229-1779241011537-0.json +1 -0
- package/coverage/tmp/coverage-79230-1779241011647-0.json +1 -0
- package/coverage/tmp/coverage-79231-1779241011765-0.json +1 -0
- package/coverage/tmp/coverage-79232-1779241011841-0.json +1 -0
- package/coverage/tmp/coverage-79233-1779241011909-0.json +1 -0
- package/coverage/tmp/coverage-79234-1779241011984-0.json +1 -0
- package/coverage/tmp/coverage-79235-1779241012055-0.json +1 -0
- package/coverage/tmp/coverage-79236-1779241012230-0.json +1 -0
- package/coverage/tmp/coverage-79237-1779241012300-0.json +1 -0
- package/coverage/tmp/coverage-79238-1779241012368-0.json +1 -0
- package/coverage/tmp/coverage-79239-1779241012438-0.json +1 -0
- package/coverage/tmp/coverage-79240-1779241012511-0.json +1 -0
- package/coverage/tmp/coverage-79241-1779241012582-0.json +1 -0
- package/coverage/tmp/coverage-79242-1779241012652-0.json +1 -0
- package/coverage/tmp/coverage-79243-1779241012814-0.json +1 -0
- package/coverage/tmp/coverage-79244-1779241012931-0.json +1 -0
- package/coverage/tmp/coverage-79245-1779241013007-0.json +1 -0
- package/coverage/tmp/coverage-79246-1779241013106-0.json +1 -0
- package/coverage/tmp/coverage-79247-1779241013178-0.json +1 -0
- package/coverage/tmp/coverage-79248-1779241013244-0.json +1 -0
- package/coverage/tmp/coverage-79249-1779241013409-0.json +1 -0
- package/coverage/tmp/coverage-79250-1779241013697-0.json +1 -0
- package/coverage/tmp/coverage-79251-1779241013847-0.json +1 -0
- package/coverage/tmp/coverage-79252-1779241014288-0.json +1 -0
- package/coverage/tmp/coverage-79253-1779241014378-0.json +1 -0
- package/coverage/tmp/coverage-79254-1779241014428-0.json +1 -0
- package/coverage/tmp/coverage-79255-1779241021774-0.json +1 -0
- package/coverage/tmp/coverage-80382-1779241021949-0.json +1 -0
- package/coverage/tmp/coverage-80383-1779241025019-0.json +1 -0
- package/coverage/tmp/coverage-80384-1779241025133-0.json +1 -0
- package/docs/Body.md +1 -0
- package/docs/Config.md +1 -0
- package/docs/Connection.md +153 -0
- package/docs/CoreConfig.md +96 -0
- package/docs/CustomReturnCodes.md +3 -0
- package/docs/HAProxy.md +62 -0
- package/docs/Header.md +1 -0
- package/docs/Logging.md +129 -0
- package/docs/Outbound.md +210 -0
- package/docs/Plugins.md +372 -0
- package/docs/Results.md +7 -0
- package/docs/Transaction.md +135 -0
- package/docs/Tutorial.md +183 -0
- package/docs/deprecated/access.md +3 -0
- package/docs/deprecated/backscatterer.md +9 -0
- package/docs/deprecated/connect.rdns_access.md +53 -0
- package/docs/deprecated/data.headers.md +3 -0
- package/docs/deprecated/data.nomsgid.md +7 -0
- package/docs/deprecated/data.noreceived.md +11 -0
- package/docs/deprecated/data.rfc5322_header_checks.md +11 -0
- package/docs/deprecated/dkim_sign.md +97 -0
- package/docs/deprecated/dkim_verify.md +28 -0
- package/docs/deprecated/dnsbl.md +80 -0
- package/docs/deprecated/dnswl.md +73 -0
- package/docs/deprecated/lookup_rdns.strict.md +67 -0
- package/docs/deprecated/mail_from.access.md +52 -0
- package/docs/deprecated/mail_from.blocklist.md +18 -0
- package/docs/deprecated/mail_from.nobounces.md +8 -0
- package/docs/deprecated/rcpt_to.access.md +53 -0
- package/docs/deprecated/rcpt_to.blocklist.md +18 -0
- package/docs/deprecated/rcpt_to.routes.md +3 -0
- package/docs/deprecated/rdns.regexp.md +30 -0
- package/docs/plugins/aliases.md +3 -0
- package/docs/plugins/auth/auth_bridge.md +34 -0
- package/docs/plugins/auth/auth_ldap.md +4 -0
- package/docs/plugins/auth/auth_proxy.md +36 -0
- package/docs/plugins/auth/auth_vpopmaild.md +33 -0
- package/docs/plugins/auth/flat_file.md +40 -0
- package/docs/plugins/block_me.md +18 -0
- package/docs/plugins/data.signatures.md +11 -0
- package/docs/plugins/delay_deny.md +23 -0
- package/docs/plugins/max_unrecognized_commands.md +6 -0
- package/docs/plugins/prevent_credential_leaks.md +22 -0
- package/docs/plugins/process_title.md +42 -0
- package/docs/plugins/queue/deliver.md +3 -0
- package/docs/plugins/queue/discard.md +32 -0
- package/docs/plugins/queue/lmtp.md +24 -0
- package/docs/plugins/queue/qmail-queue.md +16 -0
- package/docs/plugins/queue/quarantine.md +87 -0
- package/docs/plugins/queue/smtp_bridge.md +32 -0
- package/docs/plugins/queue/smtp_forward.md +127 -0
- package/docs/plugins/queue/smtp_proxy.md +68 -0
- package/docs/plugins/queue/test.md +7 -0
- package/docs/plugins/rcpt_to.in_host_list.md +34 -0
- package/docs/plugins/rcpt_to.max_count.md +3 -0
- package/docs/plugins/record_envelope_addresses.md +20 -0
- package/docs/plugins/relay.md +3 -0
- package/docs/plugins/reseed_rng.md +16 -0
- package/docs/plugins/status.md +41 -0
- package/docs/plugins/tarpit.md +50 -0
- package/docs/plugins/tls.md +235 -0
- package/docs/plugins/toobusy.md +27 -0
- package/docs/plugins/xclient.md +10 -0
- package/docs/tutorials/Migrating_from_v1_to_v2.md +96 -0
- package/docs/tutorials/SettingUpOutbound.md +62 -0
- package/eslint.config.mjs +2 -0
- package/haraka.js +74 -0
- package/haraka.sh +2 -0
- package/http/html/404.html +58 -0
- package/http/html/index.html +47 -0
- package/http/package.json +21 -0
- package/line_socket.js +24 -0
- package/logger.js +322 -0
- package/outbound/client_pool.js +59 -0
- package/outbound/config.js +134 -0
- package/outbound/hmail.js +1504 -0
- package/outbound/index.js +349 -0
- package/outbound/qfile.js +93 -0
- package/outbound/queue.js +399 -0
- package/outbound/tls.js +85 -0
- package/outbound/todo.js +17 -0
- package/package.json +99 -4
- package/plugins/.eslintrc.yaml +3 -0
- package/plugins/auth/auth_base.js +261 -0
- package/plugins/auth/auth_bridge.js +20 -0
- package/plugins/auth/auth_proxy.js +227 -0
- package/plugins/auth/auth_vpopmaild.js +162 -0
- package/plugins/auth/flat_file.js +44 -0
- package/plugins/block_me.js +88 -0
- package/plugins/data.signatures.js +30 -0
- package/plugins/delay_deny.js +153 -0
- package/plugins/prevent_credential_leaks.js +61 -0
- package/plugins/process_title.js +197 -0
- package/plugins/profile.js +11 -0
- package/plugins/queue/deliver.js +12 -0
- package/plugins/queue/discard.js +27 -0
- package/plugins/queue/lmtp.js +45 -0
- package/plugins/queue/qmail-queue.js +93 -0
- package/plugins/queue/quarantine.js +133 -0
- package/plugins/queue/smtp_bridge.js +45 -0
- package/plugins/queue/smtp_forward.js +371 -0
- package/plugins/queue/smtp_proxy.js +142 -0
- package/plugins/queue/test.js +15 -0
- package/plugins/rcpt_to.host_list_base.js +65 -0
- package/plugins/rcpt_to.in_host_list.js +56 -0
- package/plugins/record_envelope_addresses.js +17 -0
- package/plugins/reseed_rng.js +7 -0
- package/plugins/status.js +274 -0
- package/plugins/tarpit.js +45 -0
- package/plugins/tls.js +164 -0
- package/plugins/toobusy.js +47 -0
- package/plugins/xclient.js +124 -0
- package/plugins.js +604 -0
- package/queue/1772642154987_1775581346001_4_82235_TGwgfd_2_mattbook-m3.home.simerson.net +0 -0
- package/run_tests +11 -0
- package/server.js +827 -0
- package/smtp_client.js +504 -0
- package/test/.eslintrc.yaml +11 -0
- package/test/config/auth_flat_file.ini +5 -0
- package/test/config/block_me.recipient +1 -0
- package/test/config/block_me.senders +1 -0
- package/test/config/dhparams.pem +8 -0
- package/test/config/host_list +2 -0
- package/test/config/outbound_tls_cert.pem +1 -0
- package/test/config/outbound_tls_key.pem +1 -0
- package/test/config/plugins +7 -0
- package/test/config/smtp.ini +11 -0
- package/test/config/smtp_forward.ini +30 -0
- package/test/config/tls/example.com/_.example.com.key +28 -0
- package/test/config/tls/example.com/example.com.crt +25 -0
- package/test/config/tls/haraka.local.pem +51 -0
- package/test/config/tls.ini +45 -0
- package/test/config/tls_cert.pem +21 -0
- package/test/config/tls_key.pem +28 -0
- package/test/connection.js +817 -0
- package/test/fixtures/haproxy_allowed/config/connection.ini +3 -0
- package/test/fixtures/haproxy_disabled/config/connection.ini +3 -0
- package/test/fixtures/haproxy_untrusted/config/connection.ini +3 -0
- package/test/fixtures/line_socket.js +21 -0
- package/test/fixtures/todo_qfile.txt +0 -0
- package/test/fixtures/util_hmailitem.js +156 -0
- package/test/installation/config/test-plugin-flat +1 -0
- package/test/installation/config/test-plugin.ini +10 -0
- package/test/installation/config/tls.ini +1 -0
- package/test/installation/node_modules/load_first/index.js +5 -0
- package/test/installation/node_modules/load_first/package.json +11 -0
- package/test/installation/node_modules/test-plugin/config/test-plugin-flat +1 -0
- package/test/installation/node_modules/test-plugin/config/test-plugin.ini +9 -0
- package/test/installation/node_modules/test-plugin/package.json +5 -0
- package/test/installation/node_modules/test-plugin/test-plugin.js +5 -0
- package/test/installation/plugins/base_plugin.js +3 -0
- package/test/installation/plugins/folder_plugin/index.js +3 -0
- package/test/installation/plugins/folder_plugin/package.json +11 -0
- package/test/installation/plugins/inherits.js +7 -0
- package/test/installation/plugins/load_first.js +3 -0
- package/test/installation/plugins/plugin.js +1 -0
- package/test/installation/plugins/tls.js +3 -0
- package/test/logger.js +217 -0
- package/test/loud/config/dhparams.pem +0 -0
- package/test/loud/config/tls/goobered.pem +45 -0
- package/test/loud/config/tls.ini +43 -0
- package/test/mail_specimen/base64-root-part.txt +23 -0
- package/test/mail_specimen/varied-fold-lengths-preserve-data.txt +283 -0
- package/test/outbound/bounce_net_errors.js +133 -0
- package/test/outbound/bounce_rfc3464.js +226 -0
- package/test/outbound/hmail.js +210 -0
- package/test/outbound/index.js +385 -0
- package/test/outbound/qfile.js +124 -0
- package/test/outbound/queue.js +325 -0
- package/test/plugins/auth/auth_base.js +620 -0
- package/test/plugins/auth/auth_bridge.js +80 -0
- package/test/plugins/auth/auth_vpopmaild.js +81 -0
- package/test/plugins/auth/flat_file.js +123 -0
- package/test/plugins/block_me.js +141 -0
- package/test/plugins/data.signatures.js +111 -0
- package/test/plugins/delay_deny.js +262 -0
- package/test/plugins/prevent_credential_leaks.js +174 -0
- package/test/plugins/process_title.js +141 -0
- package/test/plugins/queue/deliver.js +98 -0
- package/test/plugins/queue/discard.js +78 -0
- package/test/plugins/queue/lmtp.js +137 -0
- package/test/plugins/queue/qmail-queue.js +98 -0
- package/test/plugins/queue/quarantine.js +80 -0
- package/test/plugins/queue/smtp_bridge.js +152 -0
- package/test/plugins/queue/smtp_forward.js +1023 -0
- package/test/plugins/queue/smtp_proxy.js +138 -0
- package/test/plugins/rcpt_to.host_list_base.js +102 -0
- package/test/plugins/rcpt_to.in_host_list.js +186 -0
- package/test/plugins/record_envelope_addresses.js +66 -0
- package/test/plugins/reseed_rng.js +34 -0
- package/test/plugins/status.js +207 -0
- package/test/plugins/tarpit.js +90 -0
- package/test/plugins/tls.js +86 -0
- package/test/plugins/toobusy.js +21 -0
- package/test/plugins/xclient.js +119 -0
- package/test/plugins.js +230 -0
- package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_fixed +0 -0
- package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka +0 -0
- package/test/queue/1508269674999_1508269674999_0_34002_socVUF_1_haraka +0 -0
- package/test/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka +0 -0
- package/test/queue/zero-length +0 -0
- package/test/server.js +1012 -0
- package/test/smtp_client.js +1303 -0
- package/test/tls_socket.js +321 -0
- package/test/transaction.js +554 -0
- package/tls_socket.js +771 -0
- package/transaction.js +267 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Base authentication plugin.
|
|
2
|
+
// This cannot be used on its own. You need to inherit from it.
|
|
3
|
+
// See plugins/auth/flat_file.js for an example.
|
|
4
|
+
|
|
5
|
+
// Note: You can disable setting `connection.notes.auth_passwd` by `plugin.blankout_password = true`
|
|
6
|
+
|
|
7
|
+
const crypto = require('node:crypto')
|
|
8
|
+
|
|
9
|
+
const tlds = require('haraka-tld')
|
|
10
|
+
const utils = require('haraka-utils')
|
|
11
|
+
|
|
12
|
+
const AUTH_COMMAND = 'AUTH'
|
|
13
|
+
const AUTH_METHOD_CRAM_MD5 = 'CRAM-MD5'
|
|
14
|
+
const AUTH_METHOD_PLAIN = 'PLAIN'
|
|
15
|
+
const AUTH_METHOD_LOGIN = 'LOGIN'
|
|
16
|
+
const LOGIN_STRING1 = 'VXNlcm5hbWU6' //Username: base64 coded
|
|
17
|
+
const LOGIN_STRING2 = 'UGFzc3dvcmQ6' //Password: base64 coded
|
|
18
|
+
|
|
19
|
+
exports.hook_capabilities = (next, connection) => {
|
|
20
|
+
// Don't offer AUTH capabilities unless session is encrypted
|
|
21
|
+
if (!connection.tls.enabled) return next()
|
|
22
|
+
|
|
23
|
+
const methods = ['PLAIN', 'LOGIN', 'CRAM-MD5']
|
|
24
|
+
connection.capabilities.push(`AUTH ${methods.join(' ')}`)
|
|
25
|
+
connection.notes.allowed_auth_methods = methods
|
|
26
|
+
next()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Override this at a minimum. Run cb(passwd) to provide a password.
|
|
30
|
+
exports.get_plain_passwd = (user, connection, cb) => cb()
|
|
31
|
+
|
|
32
|
+
exports.hook_unrecognized_command = function (next, connection, params) {
|
|
33
|
+
if (params[0].toUpperCase() === AUTH_COMMAND && params[1]) {
|
|
34
|
+
return this.select_auth_method(next, connection, params.slice(1).join(' '))
|
|
35
|
+
}
|
|
36
|
+
if (!connection.notes.authenticating) return next()
|
|
37
|
+
|
|
38
|
+
const am = connection.notes.auth_method
|
|
39
|
+
if (am === AUTH_METHOD_CRAM_MD5 && connection.notes.auth_ticket) {
|
|
40
|
+
return this.auth_cram_md5(next, connection, params)
|
|
41
|
+
}
|
|
42
|
+
if (am === AUTH_METHOD_LOGIN) {
|
|
43
|
+
return this.auth_login(next, connection, params)
|
|
44
|
+
}
|
|
45
|
+
if (am === AUTH_METHOD_PLAIN) {
|
|
46
|
+
return this.auth_plain(next, connection, params)
|
|
47
|
+
}
|
|
48
|
+
next()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
exports.check_plain_passwd = function (connection, user, passwd, cb) {
|
|
52
|
+
function callback(plain_pw) {
|
|
53
|
+
cb(plain_pw === null ? false : plain_pw === passwd)
|
|
54
|
+
}
|
|
55
|
+
if (this.get_plain_passwd.length == 2) {
|
|
56
|
+
this.get_plain_passwd(user, callback)
|
|
57
|
+
} else if (this.get_plain_passwd.length == 3) {
|
|
58
|
+
this.get_plain_passwd(user, connection, callback)
|
|
59
|
+
} else {
|
|
60
|
+
throw 'Invalid number of arguments for get_plain_passwd'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
exports.check_cram_md5_passwd = function (connection, user, passwd, cb) {
|
|
65
|
+
function callback(plain_pw) {
|
|
66
|
+
if (plain_pw == null) return cb(false)
|
|
67
|
+
|
|
68
|
+
const hmac = crypto.createHmac('md5', plain_pw.toString())
|
|
69
|
+
hmac.update(connection.notes.auth_ticket)
|
|
70
|
+
|
|
71
|
+
if (hmac.digest('hex') === passwd) return cb(true)
|
|
72
|
+
|
|
73
|
+
cb(false)
|
|
74
|
+
}
|
|
75
|
+
if (this.get_plain_passwd.length == 2) {
|
|
76
|
+
this.get_plain_passwd(user, callback)
|
|
77
|
+
} else if (this.get_plain_passwd.length == 3) {
|
|
78
|
+
this.get_plain_passwd(user, connection, callback)
|
|
79
|
+
} else {
|
|
80
|
+
throw 'Invalid number of arguments for get_plain_passwd'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
exports.check_user = function (next, connection, credentials, method) {
|
|
85
|
+
const plugin = this
|
|
86
|
+
connection.notes.authenticating = false
|
|
87
|
+
if (!(credentials[0] && credentials[1])) {
|
|
88
|
+
connection.respond(504, 'Invalid AUTH string', () => {
|
|
89
|
+
connection.reset_transaction(() => next(OK))
|
|
90
|
+
})
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// valid: (true|false)
|
|
95
|
+
// opts: ({ message, code }|String)
|
|
96
|
+
function passwd_ok(valid, opts) {
|
|
97
|
+
const status_code = (typeof opts === 'object' && opts.code) || (valid ? 235 : 535)
|
|
98
|
+
const status_message =
|
|
99
|
+
(typeof opts === 'object' ? opts.message : opts) ||
|
|
100
|
+
(valid ? '2.7.0 Authentication successful' : '5.7.8 Authentication failed')
|
|
101
|
+
|
|
102
|
+
// The AUTH username is attacker-controlled (base64-decoded). Strip
|
|
103
|
+
// control chars before it is stored in notes or emitted into the
|
|
104
|
+
// Authentication-Results header (header injection).
|
|
105
|
+
// eslint-disable-next-line no-control-regex
|
|
106
|
+
const safe_user = String(credentials[0] ?? '').replace(/[\x00-\x1f\x7f]/g, '')
|
|
107
|
+
|
|
108
|
+
if (valid) {
|
|
109
|
+
connection.relaying = true
|
|
110
|
+
connection.results.add({ name: 'relay' }, { pass: plugin.name })
|
|
111
|
+
|
|
112
|
+
connection.results.add(
|
|
113
|
+
{ name: 'auth' },
|
|
114
|
+
{
|
|
115
|
+
pass: plugin.name,
|
|
116
|
+
method,
|
|
117
|
+
user: safe_user,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
connection.respond(status_code, status_message, () => {
|
|
122
|
+
connection.authheader = '(authenticated bits=0)\n'
|
|
123
|
+
connection.auth_results(`auth=pass (${method.toLowerCase()})`)
|
|
124
|
+
connection.notes.auth_user = safe_user
|
|
125
|
+
if (!plugin.blankout_password) connection.notes.auth_passwd = credentials[1]
|
|
126
|
+
next(OK)
|
|
127
|
+
})
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!connection.notes.auth_fails) connection.notes.auth_fails = 0
|
|
132
|
+
|
|
133
|
+
connection.notes.auth_fails++
|
|
134
|
+
connection.results.add({ name: 'auth' }, { fail: `${plugin.name}/${method}` })
|
|
135
|
+
|
|
136
|
+
let delay = Math.pow(2, connection.notes.auth_fails - 1)
|
|
137
|
+
if (plugin.timeout && delay >= plugin.timeout) {
|
|
138
|
+
delay = plugin.timeout - 1
|
|
139
|
+
}
|
|
140
|
+
connection.lognotice(plugin, `delaying for ${delay} seconds`)
|
|
141
|
+
// here we include the username, as shown in RFC 5451 example
|
|
142
|
+
connection.auth_results(`auth=fail (${method.toLowerCase()}) smtp.auth=${safe_user}`)
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
connection.respond(status_code, status_message, () => {
|
|
145
|
+
connection.reset_transaction(() => next(OK))
|
|
146
|
+
})
|
|
147
|
+
}, delay * 1000)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (method === AUTH_METHOD_PLAIN || method === AUTH_METHOD_LOGIN) {
|
|
151
|
+
plugin.check_plain_passwd(connection, credentials[0], credentials[1], passwd_ok)
|
|
152
|
+
} else if (method === AUTH_METHOD_CRAM_MD5) {
|
|
153
|
+
plugin.check_cram_md5_passwd(connection, credentials[0], credentials[1], passwd_ok)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
exports.select_auth_method = function (next, connection, method) {
|
|
158
|
+
const split = method.split(/\s+/)
|
|
159
|
+
method = split.shift().toUpperCase()
|
|
160
|
+
if (!connection.notes.allowed_auth_methods) return next()
|
|
161
|
+
if (!connection.notes.allowed_auth_methods.includes(method)) return next()
|
|
162
|
+
|
|
163
|
+
if (connection.notes.authenticating) return next(DENYDISCONNECT, 'bad protocol')
|
|
164
|
+
|
|
165
|
+
connection.notes.authenticating = true
|
|
166
|
+
connection.notes.auth_method = method
|
|
167
|
+
|
|
168
|
+
if (method === AUTH_METHOD_PLAIN) return this.auth_plain(next, connection, split)
|
|
169
|
+
if (method === AUTH_METHOD_LOGIN) return this.auth_login(next, connection, split)
|
|
170
|
+
if (method === AUTH_METHOD_CRAM_MD5) return this.auth_cram_md5(next, connection)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
exports.auth_plain = function (next, connection, params) {
|
|
174
|
+
// one parameter given on line, either:
|
|
175
|
+
// AUTH PLAIN <param> or
|
|
176
|
+
// AUTH PLAIN\n
|
|
177
|
+
//...
|
|
178
|
+
// <param>
|
|
179
|
+
if (params[0]) {
|
|
180
|
+
const credentials = utils.unbase64(params[0]).split(/\0/)
|
|
181
|
+
credentials.shift() // Discard authid
|
|
182
|
+
this.check_user(next, connection, credentials, AUTH_METHOD_PLAIN)
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (connection.notes.auth_plain_asked_login) {
|
|
187
|
+
return next(DENYDISCONNECT, 'bad protocol')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
connection.respond(334, ' ', () => {
|
|
191
|
+
connection.notes.auth_plain_asked_login = true
|
|
192
|
+
next(OK)
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
exports.auth_login = function (next, connection, params) {
|
|
197
|
+
if (
|
|
198
|
+
(!connection.notes.auth_login_asked_login && params[0]) ||
|
|
199
|
+
(connection.notes.auth_login_asked_login && !connection.notes.auth_login_userlogin)
|
|
200
|
+
) {
|
|
201
|
+
if (!params[0]) return next(DENYDISCONNECT, 'bad protocol')
|
|
202
|
+
|
|
203
|
+
const login = utils.unbase64(params[0])
|
|
204
|
+
connection.respond(334, LOGIN_STRING2, () => {
|
|
205
|
+
connection.notes.auth_login_userlogin = login
|
|
206
|
+
connection.notes.auth_login_asked_login = true
|
|
207
|
+
next(OK)
|
|
208
|
+
})
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (connection.notes.auth_login_userlogin) {
|
|
213
|
+
const credentials = [connection.notes.auth_login_userlogin, utils.unbase64(params[0])]
|
|
214
|
+
|
|
215
|
+
connection.notes.auth_login_userlogin = null
|
|
216
|
+
connection.notes.auth_login_asked_login = false
|
|
217
|
+
|
|
218
|
+
return this.check_user(next, connection, credentials, AUTH_METHOD_LOGIN)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
connection.respond(334, LOGIN_STRING1, () => {
|
|
222
|
+
connection.notes.auth_login_asked_login = true
|
|
223
|
+
next(OK)
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
exports.auth_cram_md5 = function (next, connection, params) {
|
|
228
|
+
if (params) {
|
|
229
|
+
const credentials = utils.unbase64(params[0]).split(' ')
|
|
230
|
+
return this.check_user(next, connection, credentials, AUTH_METHOD_CRAM_MD5)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const ticket = `<${this.hexi(Math.floor(Math.random() * 1000000))}.${this.hexi(Date.now())}@${connection.local.host}>`
|
|
234
|
+
|
|
235
|
+
connection.loginfo(this, `ticket: ${ticket}`)
|
|
236
|
+
connection.respond(334, utils.base64(ticket), () => {
|
|
237
|
+
connection.notes.auth_ticket = ticket
|
|
238
|
+
next(OK)
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
exports.hexi = (number) => String(Math.abs(parseInt(number)).toString(16))
|
|
243
|
+
|
|
244
|
+
exports.constrain_sender = function (next, connection, params) {
|
|
245
|
+
if (this?.cfg?.main?.constrain_sender === false) return next()
|
|
246
|
+
|
|
247
|
+
const au = connection.results.get('auth')?.user
|
|
248
|
+
if (!au) return next()
|
|
249
|
+
|
|
250
|
+
const ad = /@/.test(au) ? au.split('@').pop() : null
|
|
251
|
+
const ed = params[0].host
|
|
252
|
+
|
|
253
|
+
if (!ad || !ed) return next()
|
|
254
|
+
|
|
255
|
+
const auth_od = tlds.get_organizational_domain(ad)
|
|
256
|
+
const envelope_od = tlds.get_organizational_domain(ed)
|
|
257
|
+
|
|
258
|
+
if (auth_od === envelope_od) return next()
|
|
259
|
+
|
|
260
|
+
next(DENY, `Envelope domain '${envelope_od}' doesn't match AUTH domain '${auth_od}'`)
|
|
261
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Bridge AUTH requests to SMTP server
|
|
2
|
+
|
|
3
|
+
exports.register = function () {
|
|
4
|
+
this.inherits('auth/auth_proxy')
|
|
5
|
+
this.load_flat_ini()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
exports.load_flat_ini = function () {
|
|
9
|
+
this.cfg = this.config.get('smtp_bridge.ini', () => {
|
|
10
|
+
this.load_flat_ini()
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
exports.check_plain_passwd = function (connection, user, passwd, cb) {
|
|
15
|
+
let { host } = this.cfg.main
|
|
16
|
+
if (this.cfg.main.port) {
|
|
17
|
+
host = `${host}:${this.cfg.main.port}`
|
|
18
|
+
}
|
|
19
|
+
this.try_auth_proxy(connection, host, user, passwd, cb)
|
|
20
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Proxy AUTH requests selectively by domain
|
|
2
|
+
|
|
3
|
+
const utils = require('haraka-utils')
|
|
4
|
+
const net_utils = require('haraka-net-utils')
|
|
5
|
+
|
|
6
|
+
const tls_socket = require('./tls_socket')
|
|
7
|
+
|
|
8
|
+
const smtp_regexp = /^(\d{3})([ -])(.*)/
|
|
9
|
+
|
|
10
|
+
exports.register = function () {
|
|
11
|
+
this.inherits('auth/auth_base')
|
|
12
|
+
this.load_tls_ini()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
exports.load_tls_ini = function () {
|
|
16
|
+
this.tls_cfg = this.config.get('tls.ini', () => {
|
|
17
|
+
this.load_tls_ini()
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
exports.hook_capabilities = (next, connection) => {
|
|
22
|
+
if (connection.tls.enabled) {
|
|
23
|
+
const methods = ['PLAIN', 'LOGIN']
|
|
24
|
+
connection.capabilities.push(`AUTH ${methods.join(' ')}`)
|
|
25
|
+
connection.notes.allowed_auth_methods = methods
|
|
26
|
+
}
|
|
27
|
+
next()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
exports.check_plain_passwd = function (connection, user, passwd, cb) {
|
|
31
|
+
let domain = /@([^@]+)$/.exec(user)
|
|
32
|
+
if (domain) {
|
|
33
|
+
domain = domain[1].toLowerCase()
|
|
34
|
+
} else {
|
|
35
|
+
// AUTH user not in user@domain.com format
|
|
36
|
+
connection.logerror(this, `AUTH user="${user}" error="not in required format"`)
|
|
37
|
+
return cb(false)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if domain exists in configuration file
|
|
41
|
+
const config = this.config.get('auth_proxy.ini')
|
|
42
|
+
if (!config.domains[domain]) {
|
|
43
|
+
connection.logerror(this, `AUTH user="${user}" error="domain '${domain}' is not defined"`)
|
|
44
|
+
return cb(false)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.try_auth_proxy(connection, config.domains[domain].split(/[,; ]/), user, passwd, cb)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
exports.try_auth_proxy = function (connection, hosts, user, passwd, cb) {
|
|
51
|
+
if (!hosts || (hosts && !hosts.length)) return cb(false)
|
|
52
|
+
if (typeof hosts !== 'object') {
|
|
53
|
+
hosts = [hosts]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const self = this
|
|
57
|
+
const ep = net_utils.endpoint(hosts.shift(), 25)
|
|
58
|
+
if (ep instanceof Error) {
|
|
59
|
+
connection.logerror(this, `invalid host: ${ep.message}`)
|
|
60
|
+
return this.try_auth_proxy(connection, hosts, user, passwd, cb)
|
|
61
|
+
}
|
|
62
|
+
const { host, port } = ep
|
|
63
|
+
let methods = []
|
|
64
|
+
let auth_complete = false
|
|
65
|
+
let auth_success = false
|
|
66
|
+
let command = 'connect'
|
|
67
|
+
let response = []
|
|
68
|
+
let secure = false
|
|
69
|
+
|
|
70
|
+
const socket = tls_socket.connect({ host, port })
|
|
71
|
+
net_utils.add_line_processor(socket)
|
|
72
|
+
connection.logdebug(this, `attempting connection to host=${host} port=${port}`)
|
|
73
|
+
socket.setTimeout(30 * 1000)
|
|
74
|
+
socket.on('connect', () => {})
|
|
75
|
+
socket.on('close', () => {
|
|
76
|
+
if (!auth_complete) {
|
|
77
|
+
// Try next host
|
|
78
|
+
return this.try_auth_proxy(connection, hosts, user, passwd, cb)
|
|
79
|
+
}
|
|
80
|
+
connection.loginfo(this, `AUTH user="${user}" host="${host}" success=${auth_success}`)
|
|
81
|
+
cb(auth_success)
|
|
82
|
+
})
|
|
83
|
+
socket.on('timeout', () => {
|
|
84
|
+
connection.logerror(this, 'connection timed out')
|
|
85
|
+
socket.end()
|
|
86
|
+
// Try next host
|
|
87
|
+
this.try_auth_proxy(connection, hosts, user, passwd, cb)
|
|
88
|
+
})
|
|
89
|
+
socket.on('error', (err) => {
|
|
90
|
+
connection.logerror(this, `connection failed to host ${host}: ${err}`)
|
|
91
|
+
socket.end()
|
|
92
|
+
})
|
|
93
|
+
socket.send_command = function (cmd, data) {
|
|
94
|
+
let line = cmd + (data ? ` ${data}` : '')
|
|
95
|
+
if (cmd === 'dot') {
|
|
96
|
+
line = '.'
|
|
97
|
+
}
|
|
98
|
+
// Don't leak proxied SASL credentials (AUTH PLAIN <base64>) to logs
|
|
99
|
+
const safe = line.replace(/^(AUTH\s+\S+\s+).+$/i, '$1[redacted]')
|
|
100
|
+
connection.logprotocol(self, `C: ${safe}`)
|
|
101
|
+
command = cmd.toLowerCase()
|
|
102
|
+
this.write(`${line}\r\n`)
|
|
103
|
+
// Clear response buffer from previous command
|
|
104
|
+
response = []
|
|
105
|
+
}
|
|
106
|
+
socket.on('line', function (line) {
|
|
107
|
+
connection.logprotocol(self, `S: ${line}`)
|
|
108
|
+
const matches = smtp_regexp.exec(line)
|
|
109
|
+
if (!matches) {
|
|
110
|
+
connection.logerror(self, `unrecognised response: ${line}`)
|
|
111
|
+
socket.end()
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const code = matches[1]
|
|
116
|
+
const cont = matches[2]
|
|
117
|
+
const rest = matches[3]
|
|
118
|
+
response.push(rest)
|
|
119
|
+
if (cont !== ' ') return
|
|
120
|
+
|
|
121
|
+
let key
|
|
122
|
+
let cert
|
|
123
|
+
|
|
124
|
+
connection.logdebug(self, `command state: ${command}`)
|
|
125
|
+
if (command === 'ehlo') {
|
|
126
|
+
if (code.startsWith('5')) {
|
|
127
|
+
// EHLO command rejected; abort
|
|
128
|
+
socket.send_command('QUIT')
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
// Parse CAPABILITIES
|
|
132
|
+
for (const i in response) {
|
|
133
|
+
if (/^STARTTLS/.test(response[i])) {
|
|
134
|
+
if (secure) continue // silly remote, we've already upgraded
|
|
135
|
+
// Opportunistic TLS: a client does not need its own
|
|
136
|
+
// certificate to negotiate TLS, so always STARTTLS when
|
|
137
|
+
// the backend offers it. The local key/cert are only
|
|
138
|
+
// attached if configured (mutual TLS), not required.
|
|
139
|
+
/* eslint no-useless-assignment: 0 */
|
|
140
|
+
key = self.config.get(self.tls_cfg.main.key || 'tls_key.pem', 'binary')
|
|
141
|
+
cert = self.config.get(self.tls_cfg.main.cert || 'tls_cert.pem', 'binary')
|
|
142
|
+
this.on('secure', () => {
|
|
143
|
+
if (secure) return
|
|
144
|
+
secure = true
|
|
145
|
+
socket.send_command('EHLO', connection.local.host)
|
|
146
|
+
})
|
|
147
|
+
socket.send_command('STARTTLS')
|
|
148
|
+
return
|
|
149
|
+
} else if (/^AUTH /.test(response[i])) {
|
|
150
|
+
// Parse supported AUTH methods
|
|
151
|
+
const parse = /^AUTH (.+)$/.exec(response[i])
|
|
152
|
+
methods = parse[1].split(/\s+/)
|
|
153
|
+
connection.logdebug(self, `found supported AUTH methods: ${methods}`)
|
|
154
|
+
// Prefer PLAIN as it's easiest
|
|
155
|
+
if (methods.includes('PLAIN')) {
|
|
156
|
+
socket.send_command('AUTH', `PLAIN ${utils.base64(`\0${user}\0${passwd}`)}`)
|
|
157
|
+
return
|
|
158
|
+
} else if (methods.includes('LOGIN')) {
|
|
159
|
+
socket.send_command('AUTH', 'LOGIN')
|
|
160
|
+
return
|
|
161
|
+
} else {
|
|
162
|
+
// No compatible methods; abort...
|
|
163
|
+
connection.logdebug(self, 'no compatible AUTH methods')
|
|
164
|
+
socket.send_command('QUIT')
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (command === 'auth') {
|
|
171
|
+
// Handle LOGIN
|
|
172
|
+
if (code.startsWith('3') && response[0] === 'VXNlcm5hbWU6') {
|
|
173
|
+
// Write to the socket directly to keep the state at 'auth'
|
|
174
|
+
this.write(`${utils.base64(user)}\r\n`)
|
|
175
|
+
response = []
|
|
176
|
+
return
|
|
177
|
+
} else if (code.startsWith('3') && response[0] === 'UGFzc3dvcmQ6') {
|
|
178
|
+
this.write(`${utils.base64(passwd)}\r\n`)
|
|
179
|
+
response = []
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
if (code.startsWith('5')) {
|
|
183
|
+
// Initial attempt failed; strip domain and retry.
|
|
184
|
+
const u = /^([^@]+)@.+$/.exec(user)
|
|
185
|
+
if (u) {
|
|
186
|
+
user = u[1]
|
|
187
|
+
if (methods.includes('PLAIN')) {
|
|
188
|
+
socket.send_command('AUTH', `PLAIN ${utils.base64(`\0${user}\0${passwd}`)}`)
|
|
189
|
+
} else if (methods.includes('LOGIN')) {
|
|
190
|
+
socket.send_command('AUTH', 'LOGIN')
|
|
191
|
+
}
|
|
192
|
+
return
|
|
193
|
+
} else {
|
|
194
|
+
// Don't attempt any other hosts
|
|
195
|
+
auth_complete = true
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (/^[345]/.test(code)) {
|
|
200
|
+
// Got an unhandled error
|
|
201
|
+
connection.logdebug(self, `error: ${line}`)
|
|
202
|
+
socket.send_command('QUIT')
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
switch (command) {
|
|
206
|
+
case 'starttls':
|
|
207
|
+
this.upgrade({ key, cert })
|
|
208
|
+
break
|
|
209
|
+
case 'connect':
|
|
210
|
+
socket.send_command('EHLO', connection.local.host)
|
|
211
|
+
break
|
|
212
|
+
case 'auth':
|
|
213
|
+
// AUTH was successful
|
|
214
|
+
auth_complete = true
|
|
215
|
+
auth_success = true
|
|
216
|
+
socket.send_command('QUIT')
|
|
217
|
+
break
|
|
218
|
+
case 'ehlo':
|
|
219
|
+
case 'helo':
|
|
220
|
+
case 'quit':
|
|
221
|
+
socket.end()
|
|
222
|
+
break
|
|
223
|
+
default:
|
|
224
|
+
throw new Error(`[auth/auth_proxy] unknown command: ${command}`)
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Auth against vpopmaild
|
|
2
|
+
|
|
3
|
+
const net = require('node:net')
|
|
4
|
+
|
|
5
|
+
exports.register = function () {
|
|
6
|
+
this.inherits('auth/auth_base')
|
|
7
|
+
this.blankout_password = true
|
|
8
|
+
|
|
9
|
+
this.load_vpopmaild_ini()
|
|
10
|
+
|
|
11
|
+
if (this.cfg.main.constrain_sender) {
|
|
12
|
+
this.register_hook('mail', 'constrain_sender')
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
exports.load_vpopmaild_ini = function () {
|
|
17
|
+
this.cfg = this.config.get(
|
|
18
|
+
'auth_vpopmaild.ini',
|
|
19
|
+
{
|
|
20
|
+
booleans: ['+main.constrain_sender'],
|
|
21
|
+
},
|
|
22
|
+
() => {
|
|
23
|
+
this.load_vpopmaild_ini()
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
exports.hook_capabilities = function (next, connection) {
|
|
29
|
+
if (!connection.tls.enabled) return next()
|
|
30
|
+
|
|
31
|
+
const methods = ['PLAIN', 'LOGIN']
|
|
32
|
+
if (this.cfg.main.sysadmin) methods.push('CRAM-MD5')
|
|
33
|
+
|
|
34
|
+
connection.capabilities.push(`AUTH ${methods.join(' ')}`)
|
|
35
|
+
connection.notes.allowed_auth_methods = methods
|
|
36
|
+
|
|
37
|
+
next()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
exports.check_plain_passwd = function (connection, user, passwd, cb) {
|
|
41
|
+
let chunk_count = 0
|
|
42
|
+
let auth_success = false
|
|
43
|
+
|
|
44
|
+
const socket = this.get_vpopmaild_socket(user)
|
|
45
|
+
socket.setEncoding('utf8')
|
|
46
|
+
|
|
47
|
+
socket.on('data', (chunk) => {
|
|
48
|
+
chunk_count++
|
|
49
|
+
if (chunk_count === 1) {
|
|
50
|
+
if (/^\+OK/.test(chunk)) {
|
|
51
|
+
socket.write(`slogin ${user} ${passwd}\n\r`)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
socket.end()
|
|
55
|
+
}
|
|
56
|
+
if (chunk_count === 2) {
|
|
57
|
+
if (/^\+OK/.test(chunk)) {
|
|
58
|
+
// slogin reply
|
|
59
|
+
auth_success = true
|
|
60
|
+
socket.write('quit\n\r')
|
|
61
|
+
}
|
|
62
|
+
socket.end() // disconnect
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
socket.on('end', () => {
|
|
67
|
+
connection.loginfo(this, `AUTH user="${user}" success=${auth_success}`)
|
|
68
|
+
cb(auth_success)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
exports.get_sock_opts = function (user) {
|
|
73
|
+
this.sock_opts = {
|
|
74
|
+
port: 89,
|
|
75
|
+
host: '127.0.0.1',
|
|
76
|
+
sysadmin: undefined,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const domain = user.split('@')[1]
|
|
80
|
+
let sect = this.cfg.main
|
|
81
|
+
if (domain && this.cfg[domain]) sect = this.cfg[domain]
|
|
82
|
+
|
|
83
|
+
if (sect.port) this.sock_opts.port = sect.port
|
|
84
|
+
if (sect.host) this.sock_opts.host = sect.host
|
|
85
|
+
if (sect.sysadmin) this.sock_opts.sysadmin = sect.sysadmin
|
|
86
|
+
|
|
87
|
+
this.logdebug(`sock: ${this.sock_opts.host}:${this.sock_opts.port}`)
|
|
88
|
+
return this.sock_opts
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
exports.get_vpopmaild_socket = function (user) {
|
|
92
|
+
this.get_sock_opts(user)
|
|
93
|
+
|
|
94
|
+
const socket = new net.Socket()
|
|
95
|
+
socket.connect(this.sock_opts.port, this.sock_opts.host)
|
|
96
|
+
socket.setTimeout(300 * 1000)
|
|
97
|
+
socket.setEncoding('utf8')
|
|
98
|
+
|
|
99
|
+
socket.on('timeout', () => {
|
|
100
|
+
this.logerror('vpopmaild connection timed out')
|
|
101
|
+
socket.end()
|
|
102
|
+
})
|
|
103
|
+
socket.on('error', (err) => {
|
|
104
|
+
this.logerror(`vpopmaild connection failed: ${err}`)
|
|
105
|
+
socket.end()
|
|
106
|
+
})
|
|
107
|
+
socket.on('connect', () => {
|
|
108
|
+
this.logdebug('vpopmail connected')
|
|
109
|
+
})
|
|
110
|
+
return socket
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
exports.get_plain_passwd = function (user, connection, cb) {
|
|
114
|
+
const socket = this.get_vpopmaild_socket(user)
|
|
115
|
+
if (!this.sock_opts.sysadmin) {
|
|
116
|
+
this.logerror('missing sysadmin credentials')
|
|
117
|
+
return cb(null)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const sys = this.sock_opts.sysadmin.split(':')
|
|
121
|
+
let plain_pass = null
|
|
122
|
+
let chunk_count = 0
|
|
123
|
+
|
|
124
|
+
socket.on('data', (chunk) => {
|
|
125
|
+
chunk_count++
|
|
126
|
+
this.logdebug(`${chunk_count}\t${chunk}`)
|
|
127
|
+
if (chunk_count === 1) {
|
|
128
|
+
if (/^\+OK/.test(chunk)) {
|
|
129
|
+
socket.write(`slogin ${sys[0]} ${sys[1]}\n\r`)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
this.logerror('no ok to start')
|
|
133
|
+
socket.end() // disconnect
|
|
134
|
+
}
|
|
135
|
+
// slogin reply
|
|
136
|
+
if (chunk_count === 2) {
|
|
137
|
+
if (/^\+OK/.test(chunk)) {
|
|
138
|
+
this.logdebug('login success, getting user info')
|
|
139
|
+
socket.write(`user_info ${user}\n\r`)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
this.logerror('syadmin login failed')
|
|
143
|
+
socket.end() // disconnect
|
|
144
|
+
}
|
|
145
|
+
if (chunk_count > 2) {
|
|
146
|
+
if (/^-ERR/.test(chunk)) {
|
|
147
|
+
this.lognotice(`get_plain failed: ${chunk}`)
|
|
148
|
+
socket.end() // disconnect
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
if (!/clear_text_password/.test(chunk)) {
|
|
152
|
+
return // pass might be in the next chunk
|
|
153
|
+
}
|
|
154
|
+
const pass = chunk.match(/clear_text_password\s(\S+)\s/)
|
|
155
|
+
plain_pass = pass[1]
|
|
156
|
+
socket.write('quit\n\r')
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
socket.on('end', () => {
|
|
160
|
+
cb(plain_pass ? plain_pass.toString() : plain_pass)
|
|
161
|
+
})
|
|
162
|
+
}
|