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.
Files changed (310) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/.githooks/pre-commit +41 -0
  3. package/.prettierignore +6 -0
  4. package/.qlty/.gitignore +7 -0
  5. package/.qlty/configs/.shellcheckrc +1 -0
  6. package/.qlty/qlty.toml +15 -0
  7. package/CHANGELOG.md +1872 -62
  8. package/CLAUDE.md +40 -0
  9. package/CONTRIBUTORS.md +34 -0
  10. package/Dockerfile +50 -0
  11. package/GEMINI.md +38 -0
  12. package/LICENSE +2 -1
  13. package/Plugins.md +227 -0
  14. package/README.md +100 -115
  15. package/SECURITY.md +178 -0
  16. package/TODO +22 -0
  17. package/address.js +53 -0
  18. package/bin/haraka +593 -0
  19. package/bin/haraka_grep +32 -0
  20. package/config/aliases +2 -0
  21. package/config/auth_flat_file.ini +7 -0
  22. package/config/auth_vpopmaild.ini +9 -0
  23. package/config/connection.ini +79 -0
  24. package/config/delay_deny.ini +7 -0
  25. package/config/dhparams.pem +8 -0
  26. package/config/host_list +3 -0
  27. package/config/host_list_regex +6 -0
  28. package/config/http.ini +11 -0
  29. package/config/lmtp.ini +7 -0
  30. package/config/log.ini +11 -0
  31. package/config/me +1 -0
  32. package/config/outbound.bounce_message +18 -0
  33. package/config/outbound.bounce_message_html +36 -0
  34. package/config/outbound.bounce_message_image +106 -0
  35. package/config/outbound.ini +24 -0
  36. package/config/plugins +67 -0
  37. package/config/smtp.ini +37 -0
  38. package/config/smtp_bridge.ini +4 -0
  39. package/config/smtp_forward.ini +31 -0
  40. package/config/smtp_proxy.ini +27 -0
  41. package/config/tarpit.timeout +1 -0
  42. package/config/tls.ini +83 -0
  43. package/config/tls_cert.pem +23 -0
  44. package/config/tls_key.pem +28 -0
  45. package/config/watch.ini +12 -0
  46. package/config/xclient.hosts +2 -0
  47. package/connection.js +1863 -0
  48. package/contrib/Haraka.cf +6 -0
  49. package/contrib/Haraka.pm +35 -0
  50. package/contrib/bad_smtp_server.pl +25 -0
  51. package/contrib/bsd-rc.d/haraka +61 -0
  52. package/contrib/debian-init.d/haraka +87 -0
  53. package/contrib/haraka.init +96 -0
  54. package/contrib/haraka.service +23 -0
  55. package/contrib/plugin2npm.sh +81 -0
  56. package/contrib/ubuntu-upstart/haraka.conf +27 -0
  57. package/coverage/coverage-final.json +2 -0
  58. package/coverage/coverage-summary.json +33 -0
  59. package/coverage/tmp/coverage-79131-1779241025146-0.json +1 -0
  60. package/coverage/tmp/coverage-79132-1779240999690-0.json +1 -0
  61. package/coverage/tmp/coverage-79172-1779241000095-0.json +1 -0
  62. package/coverage/tmp/coverage-79210-1779241000156-0.json +1 -0
  63. package/coverage/tmp/coverage-79211-1779241000209-0.json +1 -0
  64. package/coverage/tmp/coverage-79212-1779241000266-0.json +1 -0
  65. package/coverage/tmp/coverage-79213-1779241000441-0.json +1 -0
  66. package/coverage/tmp/coverage-79214-1779241000626-0.json +1 -0
  67. package/coverage/tmp/coverage-79215-1779241000795-0.json +1 -0
  68. package/coverage/tmp/coverage-79216-1779241000965-0.json +1 -0
  69. package/coverage/tmp/coverage-79218-1779241001013-0.json +1 -0
  70. package/coverage/tmp/coverage-79219-1779241001179-0.json +1 -0
  71. package/coverage/tmp/coverage-79220-1779241006249-0.json +1 -0
  72. package/coverage/tmp/coverage-79227-1779241011453-0.json +1 -0
  73. package/coverage/tmp/coverage-79229-1779241011537-0.json +1 -0
  74. package/coverage/tmp/coverage-79230-1779241011647-0.json +1 -0
  75. package/coverage/tmp/coverage-79231-1779241011765-0.json +1 -0
  76. package/coverage/tmp/coverage-79232-1779241011841-0.json +1 -0
  77. package/coverage/tmp/coverage-79233-1779241011909-0.json +1 -0
  78. package/coverage/tmp/coverage-79234-1779241011984-0.json +1 -0
  79. package/coverage/tmp/coverage-79235-1779241012055-0.json +1 -0
  80. package/coverage/tmp/coverage-79236-1779241012230-0.json +1 -0
  81. package/coverage/tmp/coverage-79237-1779241012300-0.json +1 -0
  82. package/coverage/tmp/coverage-79238-1779241012368-0.json +1 -0
  83. package/coverage/tmp/coverage-79239-1779241012438-0.json +1 -0
  84. package/coverage/tmp/coverage-79240-1779241012511-0.json +1 -0
  85. package/coverage/tmp/coverage-79241-1779241012582-0.json +1 -0
  86. package/coverage/tmp/coverage-79242-1779241012652-0.json +1 -0
  87. package/coverage/tmp/coverage-79243-1779241012814-0.json +1 -0
  88. package/coverage/tmp/coverage-79244-1779241012931-0.json +1 -0
  89. package/coverage/tmp/coverage-79245-1779241013007-0.json +1 -0
  90. package/coverage/tmp/coverage-79246-1779241013106-0.json +1 -0
  91. package/coverage/tmp/coverage-79247-1779241013178-0.json +1 -0
  92. package/coverage/tmp/coverage-79248-1779241013244-0.json +1 -0
  93. package/coverage/tmp/coverage-79249-1779241013409-0.json +1 -0
  94. package/coverage/tmp/coverage-79250-1779241013697-0.json +1 -0
  95. package/coverage/tmp/coverage-79251-1779241013847-0.json +1 -0
  96. package/coverage/tmp/coverage-79252-1779241014288-0.json +1 -0
  97. package/coverage/tmp/coverage-79253-1779241014378-0.json +1 -0
  98. package/coverage/tmp/coverage-79254-1779241014428-0.json +1 -0
  99. package/coverage/tmp/coverage-79255-1779241021774-0.json +1 -0
  100. package/coverage/tmp/coverage-80382-1779241021949-0.json +1 -0
  101. package/coverage/tmp/coverage-80383-1779241025019-0.json +1 -0
  102. package/coverage/tmp/coverage-80384-1779241025133-0.json +1 -0
  103. package/docs/Body.md +1 -0
  104. package/docs/Config.md +1 -0
  105. package/docs/Connection.md +153 -0
  106. package/docs/CoreConfig.md +96 -0
  107. package/docs/CustomReturnCodes.md +3 -0
  108. package/docs/HAProxy.md +62 -0
  109. package/docs/Header.md +1 -0
  110. package/docs/Logging.md +129 -0
  111. package/docs/Outbound.md +210 -0
  112. package/docs/Plugins.md +372 -0
  113. package/docs/Results.md +7 -0
  114. package/docs/Transaction.md +135 -0
  115. package/docs/Tutorial.md +183 -0
  116. package/docs/deprecated/access.md +3 -0
  117. package/docs/deprecated/backscatterer.md +9 -0
  118. package/docs/deprecated/connect.rdns_access.md +53 -0
  119. package/docs/deprecated/data.headers.md +3 -0
  120. package/docs/deprecated/data.nomsgid.md +7 -0
  121. package/docs/deprecated/data.noreceived.md +11 -0
  122. package/docs/deprecated/data.rfc5322_header_checks.md +11 -0
  123. package/docs/deprecated/dkim_sign.md +97 -0
  124. package/docs/deprecated/dkim_verify.md +28 -0
  125. package/docs/deprecated/dnsbl.md +80 -0
  126. package/docs/deprecated/dnswl.md +73 -0
  127. package/docs/deprecated/lookup_rdns.strict.md +67 -0
  128. package/docs/deprecated/mail_from.access.md +52 -0
  129. package/docs/deprecated/mail_from.blocklist.md +18 -0
  130. package/docs/deprecated/mail_from.nobounces.md +8 -0
  131. package/docs/deprecated/rcpt_to.access.md +53 -0
  132. package/docs/deprecated/rcpt_to.blocklist.md +18 -0
  133. package/docs/deprecated/rcpt_to.routes.md +3 -0
  134. package/docs/deprecated/rdns.regexp.md +30 -0
  135. package/docs/plugins/aliases.md +3 -0
  136. package/docs/plugins/auth/auth_bridge.md +34 -0
  137. package/docs/plugins/auth/auth_ldap.md +4 -0
  138. package/docs/plugins/auth/auth_proxy.md +36 -0
  139. package/docs/plugins/auth/auth_vpopmaild.md +33 -0
  140. package/docs/plugins/auth/flat_file.md +40 -0
  141. package/docs/plugins/block_me.md +18 -0
  142. package/docs/plugins/data.signatures.md +11 -0
  143. package/docs/plugins/delay_deny.md +23 -0
  144. package/docs/plugins/max_unrecognized_commands.md +6 -0
  145. package/docs/plugins/prevent_credential_leaks.md +22 -0
  146. package/docs/plugins/process_title.md +42 -0
  147. package/docs/plugins/queue/deliver.md +3 -0
  148. package/docs/plugins/queue/discard.md +32 -0
  149. package/docs/plugins/queue/lmtp.md +24 -0
  150. package/docs/plugins/queue/qmail-queue.md +16 -0
  151. package/docs/plugins/queue/quarantine.md +87 -0
  152. package/docs/plugins/queue/smtp_bridge.md +32 -0
  153. package/docs/plugins/queue/smtp_forward.md +127 -0
  154. package/docs/plugins/queue/smtp_proxy.md +68 -0
  155. package/docs/plugins/queue/test.md +7 -0
  156. package/docs/plugins/rcpt_to.in_host_list.md +34 -0
  157. package/docs/plugins/rcpt_to.max_count.md +3 -0
  158. package/docs/plugins/record_envelope_addresses.md +20 -0
  159. package/docs/plugins/relay.md +3 -0
  160. package/docs/plugins/reseed_rng.md +16 -0
  161. package/docs/plugins/status.md +41 -0
  162. package/docs/plugins/tarpit.md +50 -0
  163. package/docs/plugins/tls.md +235 -0
  164. package/docs/plugins/toobusy.md +27 -0
  165. package/docs/plugins/xclient.md +10 -0
  166. package/docs/tutorials/Migrating_from_v1_to_v2.md +96 -0
  167. package/docs/tutorials/SettingUpOutbound.md +62 -0
  168. package/eslint.config.mjs +2 -0
  169. package/haraka.js +74 -0
  170. package/haraka.sh +2 -0
  171. package/http/html/404.html +58 -0
  172. package/http/html/index.html +47 -0
  173. package/http/package.json +21 -0
  174. package/line_socket.js +24 -0
  175. package/logger.js +322 -0
  176. package/outbound/client_pool.js +59 -0
  177. package/outbound/config.js +134 -0
  178. package/outbound/hmail.js +1504 -0
  179. package/outbound/index.js +349 -0
  180. package/outbound/qfile.js +93 -0
  181. package/outbound/queue.js +399 -0
  182. package/outbound/tls.js +85 -0
  183. package/outbound/todo.js +17 -0
  184. package/package.json +91 -29
  185. package/plugins/.eslintrc.yaml +3 -0
  186. package/plugins/auth/auth_base.js +261 -0
  187. package/plugins/auth/auth_bridge.js +20 -0
  188. package/plugins/auth/auth_proxy.js +227 -0
  189. package/plugins/auth/auth_vpopmaild.js +162 -0
  190. package/plugins/auth/flat_file.js +44 -0
  191. package/plugins/block_me.js +88 -0
  192. package/plugins/data.signatures.js +30 -0
  193. package/plugins/delay_deny.js +153 -0
  194. package/plugins/prevent_credential_leaks.js +61 -0
  195. package/plugins/process_title.js +197 -0
  196. package/plugins/profile.js +11 -0
  197. package/plugins/queue/deliver.js +12 -0
  198. package/plugins/queue/discard.js +27 -0
  199. package/plugins/queue/lmtp.js +45 -0
  200. package/plugins/queue/qmail-queue.js +93 -0
  201. package/plugins/queue/quarantine.js +133 -0
  202. package/plugins/queue/smtp_bridge.js +45 -0
  203. package/plugins/queue/smtp_forward.js +371 -0
  204. package/plugins/queue/smtp_proxy.js +142 -0
  205. package/plugins/queue/test.js +15 -0
  206. package/plugins/rcpt_to.host_list_base.js +65 -0
  207. package/plugins/rcpt_to.in_host_list.js +56 -0
  208. package/plugins/record_envelope_addresses.js +17 -0
  209. package/plugins/reseed_rng.js +7 -0
  210. package/plugins/status.js +274 -0
  211. package/plugins/tarpit.js +45 -0
  212. package/plugins/tls.js +164 -0
  213. package/plugins/toobusy.js +47 -0
  214. package/plugins/xclient.js +124 -0
  215. package/plugins.js +604 -0
  216. package/queue/1772642154987_1775581346001_4_82235_TGwgfd_2_mattbook-m3.home.simerson.net +0 -0
  217. package/run_tests +11 -0
  218. package/server.js +827 -0
  219. package/smtp_client.js +504 -0
  220. package/test/.eslintrc.yaml +11 -0
  221. package/test/config/auth_flat_file.ini +5 -0
  222. package/test/config/block_me.recipient +1 -0
  223. package/test/config/block_me.senders +1 -0
  224. package/test/config/dhparams.pem +8 -0
  225. package/test/config/host_list +2 -0
  226. package/test/config/outbound_tls_cert.pem +1 -0
  227. package/test/config/outbound_tls_key.pem +1 -0
  228. package/test/config/plugins +7 -0
  229. package/test/config/smtp.ini +11 -0
  230. package/test/config/smtp_forward.ini +30 -0
  231. package/test/config/tls/example.com/_.example.com.key +28 -0
  232. package/test/config/tls/example.com/example.com.crt +25 -0
  233. package/test/config/tls/haraka.local.pem +51 -0
  234. package/test/config/tls.ini +45 -0
  235. package/test/config/tls_cert.pem +21 -0
  236. package/test/config/tls_key.pem +28 -0
  237. package/test/connection.js +817 -0
  238. package/test/fixtures/haproxy_allowed/config/connection.ini +3 -0
  239. package/test/fixtures/haproxy_disabled/config/connection.ini +3 -0
  240. package/test/fixtures/haproxy_untrusted/config/connection.ini +3 -0
  241. package/test/fixtures/line_socket.js +21 -0
  242. package/test/fixtures/todo_qfile.txt +0 -0
  243. package/test/fixtures/util_hmailitem.js +156 -0
  244. package/test/installation/config/test-plugin-flat +1 -0
  245. package/test/installation/config/test-plugin.ini +10 -0
  246. package/test/installation/config/tls.ini +1 -0
  247. package/test/installation/node_modules/load_first/index.js +5 -0
  248. package/test/installation/node_modules/load_first/package.json +11 -0
  249. package/test/installation/node_modules/test-plugin/config/test-plugin-flat +1 -0
  250. package/test/installation/node_modules/test-plugin/config/test-plugin.ini +9 -0
  251. package/test/installation/node_modules/test-plugin/package.json +5 -0
  252. package/test/installation/node_modules/test-plugin/test-plugin.js +5 -0
  253. package/test/installation/plugins/base_plugin.js +3 -0
  254. package/test/installation/plugins/folder_plugin/index.js +3 -0
  255. package/test/installation/plugins/folder_plugin/package.json +11 -0
  256. package/test/installation/plugins/inherits.js +7 -0
  257. package/test/installation/plugins/load_first.js +3 -0
  258. package/test/installation/plugins/plugin.js +1 -0
  259. package/test/installation/plugins/tls.js +3 -0
  260. package/test/logger.js +217 -0
  261. package/test/loud/config/dhparams.pem +0 -0
  262. package/test/loud/config/tls/goobered.pem +45 -0
  263. package/test/loud/config/tls.ini +43 -0
  264. package/test/mail_specimen/base64-root-part.txt +23 -0
  265. package/test/mail_specimen/varied-fold-lengths-preserve-data.txt +283 -0
  266. package/test/outbound/bounce_net_errors.js +133 -0
  267. package/test/outbound/bounce_rfc3464.js +226 -0
  268. package/test/outbound/hmail.js +210 -0
  269. package/test/outbound/index.js +385 -0
  270. package/test/outbound/qfile.js +124 -0
  271. package/test/outbound/queue.js +325 -0
  272. package/test/plugins/auth/auth_base.js +620 -0
  273. package/test/plugins/auth/auth_bridge.js +80 -0
  274. package/test/plugins/auth/auth_vpopmaild.js +81 -0
  275. package/test/plugins/auth/flat_file.js +123 -0
  276. package/test/plugins/block_me.js +141 -0
  277. package/test/plugins/data.signatures.js +111 -0
  278. package/test/plugins/delay_deny.js +262 -0
  279. package/test/plugins/prevent_credential_leaks.js +174 -0
  280. package/test/plugins/process_title.js +141 -0
  281. package/test/plugins/queue/deliver.js +98 -0
  282. package/test/plugins/queue/discard.js +78 -0
  283. package/test/plugins/queue/lmtp.js +137 -0
  284. package/test/plugins/queue/qmail-queue.js +98 -0
  285. package/test/plugins/queue/quarantine.js +80 -0
  286. package/test/plugins/queue/smtp_bridge.js +152 -0
  287. package/test/plugins/queue/smtp_forward.js +1023 -0
  288. package/test/plugins/queue/smtp_proxy.js +138 -0
  289. package/test/plugins/rcpt_to.host_list_base.js +102 -0
  290. package/test/plugins/rcpt_to.in_host_list.js +186 -0
  291. package/test/plugins/record_envelope_addresses.js +66 -0
  292. package/test/plugins/reseed_rng.js +34 -0
  293. package/test/plugins/status.js +207 -0
  294. package/test/plugins/tarpit.js +90 -0
  295. package/test/plugins/tls.js +86 -0
  296. package/test/plugins/toobusy.js +21 -0
  297. package/test/plugins/xclient.js +119 -0
  298. package/test/plugins.js +230 -0
  299. package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_fixed +0 -0
  300. package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka +0 -0
  301. package/test/queue/1508269674999_1508269674999_0_34002_socVUF_1_haraka +0 -0
  302. package/test/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka +0 -0
  303. package/test/queue/zero-length +0 -0
  304. package/test/server.js +1012 -0
  305. package/test/smtp_client.js +1303 -0
  306. package/test/tls_socket.js +321 -0
  307. package/test/transaction.js +554 -0
  308. package/tls_socket.js +771 -0
  309. package/transaction.js +267 -0
  310. package/lib/index.js +0 -371
@@ -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
+ }