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.
Files changed (309) 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 +1894 -0
  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 +22 -0
  13. package/Plugins.md +227 -0
  14. package/README.md +119 -4
  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 +99 -4
  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
package/server.js ADDED
@@ -0,0 +1,827 @@
1
+ 'use strict'
2
+ // smtp network server
3
+
4
+ const cluster = require('node:cluster')
5
+ const { spawn } = require('node:child_process')
6
+ const fs = require('node:fs')
7
+ const net = require('node:net')
8
+ const os = require('node:os')
9
+ const path = require('node:path')
10
+ const tls = require('node:tls')
11
+ const constants = require('haraka-constants')
12
+ const net_utils = require('haraka-net-utils')
13
+
14
+ const { endpoint } = require('haraka-net-utils')
15
+ const tls_socket = require('./tls_socket')
16
+ const conn = require('./connection')
17
+ const outbound = require('./outbound')
18
+
19
+ const Server = exports
20
+ Server.logger = require('./logger')
21
+ Server.config = require('haraka-config')
22
+ Server.plugins = require('./plugins')
23
+ Server.notes = {}
24
+
25
+ const { logger } = Server
26
+
27
+ // Need these here so we can run hooks
28
+ logger.add_log_methods(Server, 'server')
29
+
30
+ Server.listeners = []
31
+
32
+ Server.load_smtp_ini = () => {
33
+ Server.cfg = Server.config.get(
34
+ 'smtp.ini',
35
+ {
36
+ booleans: ['-main.daemonize', '-main.graceful_shutdown'],
37
+ },
38
+ () => {
39
+ Server.load_smtp_ini()
40
+ },
41
+ )
42
+
43
+ if (Server.cfg.main.nodes === undefined) {
44
+ Server.logwarn(`smtp.ini.nodes unset, using 1, see https://github.com/haraka/Haraka/wiki/Performance-Tuning`)
45
+ }
46
+
47
+ const defaults = {
48
+ inactivity_timeout: 300,
49
+ daemon_log_file: '/var/log/haraka.log',
50
+ daemon_pid_file: '/var/run/haraka.pid',
51
+ force_shutdown_timeout: 30,
52
+ smtps_port: 465,
53
+ nodes: 1,
54
+ }
55
+
56
+ for (const key in defaults) {
57
+ if (Server.cfg.main[key] !== undefined) continue
58
+ Server.cfg.main[key] = defaults[key]
59
+ }
60
+ }
61
+
62
+ Server.load_http_ini = () => {
63
+ Server.http = {}
64
+ Server.http.cfg = Server.config.get('http.ini', () => {
65
+ Server.load_http_ini()
66
+ }).main
67
+ }
68
+
69
+ Server.load_connection_ini = () => {
70
+ Server.connection = {}
71
+ Server.connection.cfg = Server.config.get('connection.ini', {
72
+ booleans: ['+haproxy.enabled'],
73
+ })
74
+ }
75
+
76
+ Server.load_smtp_ini()
77
+ Server.load_http_ini()
78
+ Server.load_connection_ini()
79
+
80
+ Server.daemonize = function () {
81
+ const c = this.cfg.main
82
+ if (!c.daemonize) return
83
+
84
+ if (!process.env.__daemon) {
85
+ // Remove process.on('exit') listeners otherwise
86
+ // we get a spurious 'Exiting' log entry.
87
+ process.removeAllListeners('exit')
88
+ Server.lognotice('Daemonizing...')
89
+
90
+ const log_fd = fs.openSync(c.daemon_log_file, 'a')
91
+ const child = spawn(process.execPath, process.argv.slice(1), {
92
+ detached: true,
93
+ stdio: ['ignore', log_fd, log_fd],
94
+ env: { ...process.env, __daemon: '1' },
95
+ cwd: process.cwd(),
96
+ })
97
+ child.unref()
98
+ process.exit(0)
99
+ }
100
+
101
+ // We are the daemon from here on...
102
+ try {
103
+ fs.writeFileSync(c.daemon_pid_file, `${process.pid}\n`, { flag: 'wx' })
104
+ process.on('exit', () => {
105
+ try {
106
+ fs.unlinkSync(c.daemon_pid_file)
107
+ } catch {}
108
+ })
109
+ } catch (err) {
110
+ Server.logerror(err.message)
111
+ logger.dump_and_exit(1)
112
+ }
113
+ }
114
+
115
+ Server.flushQueue = async (domain) => {
116
+ if (!Server.cluster) {
117
+ await outbound.flush_queue(domain)
118
+ return
119
+ }
120
+
121
+ for (const id in cluster.workers) {
122
+ cluster.workers[id].send({ event: 'outbound.flush_queue', domain })
123
+ }
124
+ }
125
+
126
+ let graceful_in_progress = false
127
+
128
+ Server.gracefulRestart = () => {
129
+ Server._graceful()
130
+ }
131
+
132
+ Server.stopListeners = () => {
133
+ Server.loginfo('Shutting down listeners')
134
+ for (const l of Server.listeners) {
135
+ l.close()
136
+ }
137
+ Server.listeners = []
138
+ }
139
+
140
+ Server.performShutdown = () => {
141
+ if (Server.cfg.main.graceful_shutdown) {
142
+ return Server.gracefulShutdown()
143
+ }
144
+ Server.loginfo('Shutting down.')
145
+ process.exit(0)
146
+ }
147
+
148
+ Server.gracefulShutdown = () => {
149
+ Server.stopListeners()
150
+ Server._graceful(() => {
151
+ // log();
152
+ Server.loginfo('Failed to shutdown naturally. Exiting.')
153
+ process.exit(0)
154
+ })
155
+ }
156
+
157
+ Server._graceful = async (shutdown) => {
158
+ if (!Server.cluster && shutdown) {
159
+ for (const module of ['outbound', 'cfreader', 'plugins']) {
160
+ process.emit('message', { event: `${module}.shutdown` })
161
+ }
162
+ const t = setTimeout(shutdown, Server.cfg.main.force_shutdown_timeout * 1000)
163
+ return t.unref()
164
+ }
165
+
166
+ if (graceful_in_progress) {
167
+ Server.lognotice('Restart currently in progress - ignoring request')
168
+ return
169
+ }
170
+
171
+ graceful_in_progress = true
172
+ // TODO: Make these configurable
173
+ const disconnect_timeout = 30
174
+ const exit_timeout = 30
175
+ cluster.removeAllListeners('exit')
176
+
177
+ // we reload using eachLimit where limit = num_workers - 1
178
+ // this kills all-but-one workers in parallel, leaving one running
179
+ // for new connections, and then restarts that one last worker.
180
+
181
+ const worker_ids = Object.keys(cluster.workers)
182
+ let limit = worker_ids.length - 1
183
+ if (limit < 2) limit = 1
184
+
185
+ const todo = []
186
+
187
+ for (const id of Object.keys(cluster.workers)) {
188
+ todo.push(() => {
189
+ return new Promise((resolve) => {
190
+ Server.lognotice(`Killing worker: ${id}`)
191
+ const worker = cluster.workers[id]
192
+ for (const module of ['outbound', 'cfreader', 'plugins']) {
193
+ worker.send({ event: `${module}.shutdown` })
194
+ }
195
+ worker.disconnect()
196
+ let disconnect_received = false
197
+ const disconnect_timer = setTimeout(() => {
198
+ if (!disconnect_received) {
199
+ Server.logcrit('Disconnect never received by worker. Killing.')
200
+ worker.kill()
201
+ }
202
+ }, disconnect_timeout * 1000)
203
+
204
+ worker.once('disconnect', () => {
205
+ clearTimeout(disconnect_timer)
206
+ disconnect_received = true
207
+ Server.lognotice('Disconnect complete')
208
+ let dead = false
209
+ const timer = setTimeout(() => {
210
+ if (!dead) {
211
+ Server.logcrit(`Worker ${id} failed to shutdown. Killing.`)
212
+ worker.kill()
213
+ }
214
+ }, exit_timeout * 1000)
215
+ worker.once('exit', () => {
216
+ dead = true
217
+ clearTimeout(timer)
218
+ if (shutdown) resolve()
219
+ })
220
+ })
221
+ if (!shutdown) {
222
+ const newWorker = cluster.fork()
223
+ newWorker.once('listening', () => {
224
+ Server.lognotice('Replacement worker online.')
225
+ newWorker.on('exit', (code, signal) => {
226
+ cluster_exit_listener(newWorker, code, signal)
227
+ })
228
+ resolve()
229
+ })
230
+ }
231
+ })
232
+ })
233
+ }
234
+
235
+ while (todo.length) {
236
+ // process batches of workers: invoke each queued thunk so we
237
+ // actually await the worker shutdown promises (passing the bare
238
+ // functions to Promise.all would resolve immediately).
239
+ await Promise.all(todo.splice(0, limit).map((fn) => fn()))
240
+ }
241
+
242
+ if (shutdown) {
243
+ Server.loginfo('Workers closed. Shutting down master process subsystems')
244
+ for (const module of ['outbound', 'cfreader', 'plugins']) {
245
+ process.emit('message', { event: `${module}.shutdown` })
246
+ }
247
+ const t2 = setTimeout(shutdown, Server.cfg.main.force_shutdown_timeout * 1000)
248
+ return t2.unref()
249
+ }
250
+ graceful_in_progress = false
251
+ Server.lognotice(`Reload complete, workers: ${JSON.stringify(Object.keys(cluster.workers))}`)
252
+ }
253
+
254
+ Server.sendToMaster = (command, params) => {
255
+ // console.log("Send to master: ", command);
256
+ if (Server.cluster) {
257
+ if (Server.cluster.isMaster) {
258
+ Server.receiveAsMaster(command, params)
259
+ } else {
260
+ process.send({ cmd: command, params })
261
+ }
262
+ } else {
263
+ Server.receiveAsMaster(command, params)
264
+ }
265
+ }
266
+
267
+ Server.receiveAsMaster = (command, params) => {
268
+ if (!Server[command]) {
269
+ Server.logerror(`Invalid command: ${command}`)
270
+ return
271
+ }
272
+ Server[command].apply(Server, params)
273
+ }
274
+
275
+ function messageHandler(worker, msg) {
276
+ if (msg?.cmd) {
277
+ Server.receiveAsMaster(msg.cmd, msg.params)
278
+ }
279
+ }
280
+
281
+ Server.get_listen_addrs = (cfg, port) => {
282
+ if (!port) port = 25
283
+ let listeners = []
284
+ if (cfg?.listen) {
285
+ listeners = cfg.listen.split(/\s*,\s*/)
286
+ if (listeners[0] === '') listeners = []
287
+ for (let i = 0; i < listeners.length; i++) {
288
+ const ep = endpoint(listeners[i], port)
289
+ if (ep instanceof Error) continue
290
+ listeners[i] = ep.toString()
291
+ }
292
+ }
293
+ if (cfg.port) {
294
+ let host = cfg.listen_host
295
+ if (!host) {
296
+ host = '[::0]'
297
+ Server.default_host = true
298
+ }
299
+ listeners.unshift(`${host}:${cfg.port}`)
300
+ }
301
+ if (listeners.length) return listeners
302
+
303
+ Server.default_host = true
304
+ listeners.push(`[::0]:${port}`)
305
+
306
+ return listeners
307
+ }
308
+
309
+ Server.createServer = (params) => {
310
+ const c = Server.cfg.main
311
+ for (const key in params) {
312
+ if (typeof params[key] === 'function') continue
313
+ c[key] = params[key]
314
+ }
315
+
316
+ Server.notes = {}
317
+ Server.plugins.server = Server
318
+ Server.plugins.load_plugins()
319
+
320
+ const inactivity_timeout = (c.inactivity_timeout || 300) * 1000
321
+
322
+ if (!cluster || !c.nodes) {
323
+ Server.daemonize(c)
324
+ Server.setup_smtp_listeners(Server.plugins, 'master', inactivity_timeout)
325
+ return
326
+ }
327
+
328
+ // Cluster
329
+ Server.cluster = cluster
330
+
331
+ // Cluster Workers
332
+ if (!cluster.isMaster) {
333
+ Server.setup_smtp_listeners(Server.plugins, 'child', inactivity_timeout)
334
+ return
335
+ } else {
336
+ // console.log("Setting up message handler");
337
+ cluster.on('message', messageHandler)
338
+ }
339
+
340
+ // Cluster Master
341
+ // We fork workers in init_master_respond so that plugins
342
+ // can put handlers on cluster events before they are emitted.
343
+ Server.plugins.run_hooks('init_master', Server)
344
+ }
345
+
346
+ Server.load_default_tls_config = (done) => {
347
+ // this fn exists solely for testing
348
+ if (Server.config.root_path != tls_socket.config.root_path) {
349
+ Server.loginfo(`resetting tls_config.config path to ${Server.config.root_path}`)
350
+ tls_socket.config = tls_socket.config.module_config(path.dirname(Server.config.root_path))
351
+ }
352
+ tls_socket.getSocketOpts('*').then((opts) => {
353
+ done(opts)
354
+ })
355
+ }
356
+
357
+ Server.create_smtps_server = (opts, onConnect) => {
358
+ let server
359
+ const socket_tls_state = new Map()
360
+ const proxyPrefix = Buffer.from('PROXY ')
361
+ // Defensive cap while waiting for a PROXY v1 line before TLS starts.
362
+ const proxyLineReadLimit = 512
363
+
364
+ const tlsServer = tls.createServer(opts, (cleartext) => {
365
+ const state_key = socket_tls_state_key(cleartext)
366
+ const smtps_state = socket_tls_state.get(state_key)
367
+ if (smtps_state) cleartext.haraka_smtps = smtps_state
368
+ socket_tls_state.delete(state_key)
369
+
370
+ onConnect(cleartext)
371
+ })
372
+
373
+ function close_with_proxy_error(socket, timer, msg) {
374
+ clearTimeout(timer)
375
+ socket.removeAllListeners('data')
376
+ socket.end(`421 ${msg}\r\n`, () => {
377
+ socket.destroy()
378
+ })
379
+ }
380
+
381
+ function socket_tls_state_key(socket) {
382
+ return JSON.stringify([socket.remoteAddress, socket.remotePort, socket.localAddress, socket.localPort])
383
+ }
384
+
385
+ function start_tls(socket, proxy, peer_allowed) {
386
+ if (proxy || peer_allowed) {
387
+ const smtps_state = { peer_allowed }
388
+ if (proxy) {
389
+ smtps_state.proxy = {
390
+ ...proxy,
391
+ proxy_ip: net_utils.normalize_ip(socket.remoteAddress) || socket.remoteAddress,
392
+ }
393
+ }
394
+ socket_tls_state.set(socket_tls_state_key(socket), smtps_state)
395
+ }
396
+
397
+ tlsServer.emit('connection', socket)
398
+ }
399
+
400
+ tlsServer.on('tlsClientError', (err, cleartext) => {
401
+ if (cleartext) socket_tls_state.delete(socket_tls_state_key(cleartext))
402
+ server.emit('tlsClientError', err, cleartext)
403
+ })
404
+
405
+ tlsServer.on('secureConnection', (cleartext) => {
406
+ server.emit('secureConnection', cleartext)
407
+ })
408
+
409
+ function starts_with_proxy_prefix(data) {
410
+ if (!data.length) return true
411
+ if (data.length > proxyPrefix.length) return data.subarray(0, proxyPrefix.length).equals(proxyPrefix)
412
+
413
+ return proxyPrefix.subarray(0, data.length).equals(data)
414
+ }
415
+
416
+ function start_tls_with_buffer(socket, data, proxy, peer_allowed) {
417
+ // Preserve bytes already read by the PROXY pre-parser, then hand the
418
+ // paused socket to TLS before letting it read again.
419
+ socket.pause()
420
+ if (data?.length) socket.unshift(data)
421
+ setImmediate(() => {
422
+ start_tls(socket, proxy, peer_allowed)
423
+ socket.resume()
424
+ })
425
+ }
426
+
427
+ server = net.createServer((socket) => {
428
+ const remote_ip = net_utils.normalize_ip(socket.remoteAddress) || socket.remoteAddress
429
+
430
+ if (!net_utils.is_haproxy_allowed(remote_ip)) {
431
+ start_tls(socket)
432
+ return
433
+ }
434
+
435
+ let current_data = null
436
+ const proxy_timer = setTimeout(() => {
437
+ close_with_proxy_error(socket, proxy_timer, 'PROXY timeout')
438
+ }, 30 * 1000)
439
+
440
+ function cleanup() {
441
+ clearTimeout(proxy_timer)
442
+ // Stop flowing before removing the pre-parser listener so TLS bytes
443
+ // cannot arrive between listener removal and TLS attachment.
444
+ socket.pause()
445
+ socket.removeListener('data', on_data)
446
+ socket.removeListener('close', cleanup)
447
+ socket.removeListener('error', cleanup)
448
+ }
449
+
450
+ function on_data(data) {
451
+ current_data = current_data ? Buffer.concat([current_data, data]) : data
452
+
453
+ if (!starts_with_proxy_prefix(current_data)) {
454
+ cleanup()
455
+ start_tls_with_buffer(socket, current_data, null, true)
456
+ return
457
+ }
458
+
459
+ const offset = current_data.indexOf(0x0a)
460
+ if (offset === -1) {
461
+ if (current_data.length > proxyLineReadLimit) {
462
+ close_with_proxy_error(socket, proxy_timer, 'Invalid PROXY format')
463
+ }
464
+ return
465
+ }
466
+ if (offset > proxyLineReadLimit) {
467
+ close_with_proxy_error(socket, proxy_timer, 'Invalid PROXY format')
468
+ return
469
+ }
470
+
471
+ cleanup()
472
+
473
+ const proxy = net_utils.parse_proxy_line(current_data.slice(0, offset + 1))
474
+ if (!proxy) {
475
+ close_with_proxy_error(socket, proxy_timer, 'Invalid PROXY format')
476
+ return
477
+ }
478
+
479
+ const rest = current_data.slice(offset + 1)
480
+ start_tls_with_buffer(socket, rest, proxy, true)
481
+ }
482
+
483
+ socket.once('close', cleanup)
484
+ socket.once('error', cleanup)
485
+ socket.on('data', on_data)
486
+ })
487
+
488
+ server.tlsServer = tlsServer
489
+
490
+ return server
491
+ }
492
+
493
+ Server.get_smtp_server = async (ep, inactivity_timeout) => {
494
+ let server
495
+
496
+ function onConnect(client) {
497
+ client.setTimeout(inactivity_timeout)
498
+ const connection = conn.createConnection(client, server, Server.cfg)
499
+
500
+ if (server.has_tls) {
501
+ const cipher = client.getCipher()
502
+ cipher.version = client.getProtocol() // replace min with actual
503
+
504
+ connection.setTLS({
505
+ cipher,
506
+ verified: client.authorized,
507
+ verifyError: client.authorizationError,
508
+ peerCertificate: client.getPeerCertificate(),
509
+ })
510
+ }
511
+
512
+ if (client.haraka_smtps?.proxy) connection.apply_proxy(client.haraka_smtps.proxy)
513
+ }
514
+
515
+ if (ep.port === parseInt(Server.cfg.main.smtps_port, 10)) {
516
+ Server.loginfo('getting SocketOpts for SMTPS server')
517
+ const opts = await tls_socket.getSocketOpts('*')
518
+ Server.loginfo(`Creating TLS server on ${ep}`)
519
+
520
+ opts.rejectUnauthorized = tls_socket.get_rejectUnauthorized(
521
+ opts.rejectUnauthorized,
522
+ ep.port,
523
+ tls_socket.cfg.main.requireAuthorized,
524
+ )
525
+
526
+ server = Server.connection.cfg.haproxy.enabled
527
+ ? Server.create_smtps_server(opts, onConnect)
528
+ : tls.createServer(opts, onConnect)
529
+ const tls_event_server = server.tlsServer || server
530
+ tls_socket.addOCSP(tls_event_server)
531
+ server.has_tls = true
532
+ tls_event_server.on('resumeSession', (id, rsDone) => {
533
+ Server.loginfo('client requested TLS resumeSession')
534
+ rsDone(null, null)
535
+ })
536
+ Server.listeners.push(server)
537
+ return server
538
+ } else {
539
+ server = tls_socket.createServer(onConnect)
540
+ server.has_tls = false
541
+ await tls_socket.getSocketOpts('*')
542
+ Server.listeners.push(server)
543
+ return server
544
+ }
545
+ }
546
+
547
+ Server.setup_smtp_listeners = async (plugins2, type, inactivity_timeout) => {
548
+ const errors = []
549
+
550
+ for (const [, ifObj] of Object.entries(os.networkInterfaces())) {
551
+ for (const addr of ifObj) {
552
+ if (addr.family === 'IPv6') {
553
+ if (!Server.notes.IPv6) Server.notes.IPv6 = true
554
+ } else if (addr.family === 'IPv4') {
555
+ if (!Server.notes.IPv4) Server.notes.IPv4 = true
556
+ } else {
557
+ console.error(addr)
558
+ }
559
+ }
560
+ }
561
+
562
+ for (const listen_address of Server.get_listen_addrs(Server.cfg.main)) {
563
+ const ep = endpoint(listen_address, 25)
564
+
565
+ if (ep instanceof Error) {
566
+ Server.logerror(`Invalid "listen" format in smtp.ini: ${listen_address}`)
567
+ continue
568
+ }
569
+
570
+ const server = await Server.get_smtp_server(ep, inactivity_timeout)
571
+ if (!server) continue
572
+
573
+ server.notes = Server.notes
574
+ if (Server.cluster) server.cluster = Server.cluster
575
+
576
+ server
577
+ .on('listening', function () {
578
+ const addr = this.address()
579
+ Server.lognotice(`Listening on ${endpoint(addr)}`)
580
+ })
581
+ .on('close', () => {
582
+ Server.loginfo(`Listener ${ep} stopped`)
583
+ })
584
+ .on('error', (e) => {
585
+ errors.push(e)
586
+ Server.logerror(`Failed to setup listeners: ${e.message}`)
587
+ if (e.code !== 'EAFNOSUPPORT') {
588
+ Server.logerror(e)
589
+ return
590
+ }
591
+ // Fallback from IPv6 to IPv4 if not supported
592
+ // But only if we supplied the default of [::0]:25
593
+ if (/^::0/.test(ep.host) && Server.default_host) {
594
+ server.listen(ep.port, '0.0.0.0', 0)
595
+ return
596
+ }
597
+ // Pass error to callback
598
+ Server.logerror(e)
599
+ })
600
+
601
+ await ep.bind(server, { backlog: 0 })
602
+ }
603
+
604
+ if (errors.length) {
605
+ for (const e of errors) {
606
+ Server.logerror(`Failed to setup listeners: ${e.message}`)
607
+ }
608
+ return logger.dump_and_exit(-1)
609
+ }
610
+ Server.listening()
611
+ plugins2.run_hooks(`init_${type}`, Server)
612
+ }
613
+
614
+ Server.setup_http_listeners = async () => {
615
+ if (!Server.http?.cfg?.listen) return
616
+
617
+ const listeners = Server.get_listen_addrs(Server.http.cfg, 80)
618
+ if (!listeners.length) return
619
+
620
+ try {
621
+ Server.http.express = require('express')
622
+ Server.loginfo('express loaded at Server.http.express')
623
+ } catch {
624
+ Server.logerror('express failed to load. No http server. Install express with: npm install -g express')
625
+ return
626
+ }
627
+
628
+ const app = Server.http.express()
629
+ Server.http.app = app
630
+ Server.loginfo('express app is at Server.http.app')
631
+
632
+ for (const listen_address of listeners) {
633
+ const ep = endpoint(listen_address, 80)
634
+ if (ep instanceof Error) {
635
+ Server.logerror(`Invalid format for listen in http.ini: ${listen_address}`)
636
+ continue
637
+ }
638
+
639
+ if (443 == ep.port) {
640
+ const tlsOpts = { ...tls_socket.certsByHost['*'] }
641
+ tlsOpts.requestCert = false // not appropriate for HTTPS
642
+ Server.http.server = require('https').createServer(tlsOpts, app)
643
+ } else {
644
+ Server.http.server = require('http').createServer(app)
645
+ }
646
+
647
+ Server.listeners.push(Server.http.server)
648
+
649
+ Server.http.server.on('listening', function () {
650
+ Server.lognotice(`Listening on ${endpoint(this.address())}`)
651
+ })
652
+
653
+ Server.http.server.on('error', (e) => {
654
+ Server.logerror(e)
655
+ })
656
+
657
+ await ep.bind(Server.http.server, { backlog: 0 })
658
+ }
659
+
660
+ Server.plugins.run_hooks('init_http', Server)
661
+ app.use(Server.http.express.static(Server.get_http_docroot()))
662
+ app.use(Server.handle404)
663
+ }
664
+
665
+ Server.init_master_respond = async (retval, msg) => {
666
+ if (!(retval === constants.ok || retval === constants.cont)) {
667
+ Server.logerror(`init_master returned error${msg ? `: ${msg}` : ''}`)
668
+ return logger.dump_and_exit(1)
669
+ }
670
+
671
+ const c = Server.cfg.main
672
+ Server.ready = 1
673
+
674
+ // Load the queue if we're just one process
675
+ if (!(cluster && c.nodes)) {
676
+ try {
677
+ await outbound.init_queue()
678
+ } catch {
679
+ Server.logcrit('Loading queue failed. Shutting down.')
680
+ return logger.dump_and_exit(1)
681
+ }
682
+ Server.setup_http_listeners()
683
+ return
684
+ }
685
+
686
+ // Running under cluster, fork children here, so that
687
+ // cluster events can be registered in init_master hooks.
688
+ try {
689
+ const pids = await outbound.scan_queue_pids()
690
+ Server.daemonize()
691
+ // Fork workers
692
+ const workers = c.nodes === 'cpus' ? os.cpus().length : c.nodes
693
+ const new_workers = []
694
+ for (let i = 0; i < workers; i++) {
695
+ new_workers.push(cluster.fork({ CLUSTER_MASTER_PID: process.pid }))
696
+ }
697
+ for (let j = 0; j < pids.length; j++) {
698
+ new_workers[j % new_workers.length].send({
699
+ event: 'outbound.load_pid_queue',
700
+ data: pids[j],
701
+ })
702
+ }
703
+ cluster.on('online', (worker) => {
704
+ Server.lognotice('worker started', {
705
+ worker: worker.id,
706
+ pid: worker.process.pid,
707
+ })
708
+ })
709
+ cluster.on('listening', (worker, address) => {
710
+ Server.lognotice(`worker ${worker.id} listening on ${endpoint(address)}`)
711
+ })
712
+ cluster.on('exit', cluster_exit_listener)
713
+ } catch {
714
+ Server.logcrit('Scanning queue failed. Shutting down.')
715
+ logger.dump_and_exit(1)
716
+ }
717
+ }
718
+
719
+ function cluster_exit_listener(worker, code, signal) {
720
+ if (signal) {
721
+ Server.lognotice(`worker ${worker.id} killed by signal ${signal}`)
722
+ } else if (code !== 0) {
723
+ Server.lognotice(`worker ${worker.id} exited with error code: ${code}`)
724
+ }
725
+ if (signal || code !== 0) {
726
+ // Restart worker
727
+ const new_worker = cluster.fork({
728
+ CLUSTER_MASTER_PID: process.pid,
729
+ })
730
+ new_worker.send({
731
+ event: 'outbound.load_pid_queue',
732
+ data: worker.process.pid,
733
+ })
734
+ }
735
+ }
736
+
737
+ Server.init_child_respond = (retval, msg) => {
738
+ switch (retval) {
739
+ case constants.ok:
740
+ case constants.cont:
741
+ Server.setup_http_listeners()
742
+ return
743
+ }
744
+
745
+ const pid = process.env.CLUSTER_MASTER_PID
746
+ Server.logerror(`init_child returned error ${msg ? `: ${msg}` : ''}`)
747
+ try {
748
+ if (pid) {
749
+ process.kill(pid)
750
+ Server.logerror(`Killing master (pid=${pid})`)
751
+ }
752
+ } catch {
753
+ Server.logerror('Terminating child')
754
+ }
755
+ logger.dump_and_exit(1)
756
+ }
757
+
758
+ Server.listening = () => {
759
+ const c = Server.cfg.main
760
+
761
+ // Drop privileges
762
+ if (c.group) {
763
+ Server.lognotice(`Switching from current gid: ${process.getgid()}`)
764
+ process.setgid(c.group)
765
+ Server.lognotice(`New gid: ${process.getgid()}`)
766
+ }
767
+ if (c.user) {
768
+ Server.lognotice(`Switching from current uid: ${process.getuid()}`)
769
+ process.setuid(c.user)
770
+ Server.lognotice(`New uid: ${process.getuid()}`)
771
+ }
772
+
773
+ Server.ready = 1
774
+ }
775
+
776
+ Server.init_http_respond = () => {
777
+ Server.loginfo('init_http_respond')
778
+
779
+ let WebSocketServer
780
+ try {
781
+ WebSocketServer = require('ws').Server
782
+ } catch {
783
+ Server.logerror(`unable to load ws.\n did you: npm install -g ws?`)
784
+ return
785
+ }
786
+
787
+ if (!WebSocketServer) {
788
+ Server.logerror('ws failed to load')
789
+ return
790
+ }
791
+
792
+ Server.http.wss = new WebSocketServer({ server: Server.http.server })
793
+ Server.loginfo('Server.http.wss loaded')
794
+
795
+ Server.plugins.run_hooks('init_wss', Server)
796
+ }
797
+
798
+ Server.init_wss_respond = () => {
799
+ Server.loginfo('init_wss_respond')
800
+ }
801
+
802
+ Server.get_http_docroot = () => {
803
+ if (Server.http.cfg.docroot) return Server.http.cfg.docroot
804
+
805
+ Server.http.cfg.docroot = path.join(process.env.HARAKA || __dirname, 'http', 'html')
806
+ Server.loginfo(`using html docroot: ${Server.http.cfg.docroot}`)
807
+ return Server.http.cfg.docroot
808
+ }
809
+
810
+ Server.handle404 = (req, res) => {
811
+ // abandon all hope, serve up a 404
812
+ const docroot = Server.get_http_docroot()
813
+
814
+ // respond with html page
815
+ if (req.accepts('html')) {
816
+ res.status(404).sendFile('404.html', { root: docroot })
817
+ return
818
+ }
819
+
820
+ // respond with json
821
+ if (req.accepts('json')) {
822
+ res.status(404).send({ err: 'Not found' })
823
+ return
824
+ }
825
+
826
+ res.status(404).send('Not found!')
827
+ }