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
@@ -0,0 +1,349 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs/promises')
4
+ const path = require('node:path')
5
+
6
+ const { Address } = require('../address')
7
+ const config = require('haraka-config')
8
+ const constants = require('haraka-constants')
9
+ const net_utils = require('haraka-net-utils')
10
+ const utils = require('haraka-utils')
11
+ const ResultStore = require('haraka-results')
12
+
13
+ const logger = require('../logger')
14
+ const trans = require('../transaction')
15
+ const plugins = require('../plugins')
16
+ const FsyncWriteStream = utils.FsyncWriteStream
17
+
18
+ const obc = require('./config')
19
+ const queuelib = require('./queue')
20
+ const HMailItem = require('./hmail')
21
+ const TODOItem = require('./todo')
22
+ const _qfile = (exports.qfile = require('./qfile'))
23
+
24
+ const { queue_dir, temp_fail_queue, delivery_queue } = queuelib
25
+
26
+ const smtp_ini = config.get('smtp.ini', {
27
+ booleans: ['+headers.add_received'],
28
+ })
29
+
30
+ exports.temp_fail_queue = temp_fail_queue
31
+ exports.delivery_queue = delivery_queue
32
+
33
+ exports.name = 'outbound'
34
+ exports.net_utils = net_utils
35
+ exports.config = config
36
+
37
+ const qlfns = [
38
+ 'get_stats',
39
+ 'list_queue',
40
+ 'stat_queue',
41
+ 'scan_queue_pids',
42
+ 'flush_queue',
43
+ 'load_pid_queue',
44
+ 'ensure_queue_dir',
45
+ 'init_queue',
46
+ 'stats',
47
+ ]
48
+ for (const n of qlfns) {
49
+ exports[n] = queuelib[n]
50
+ }
51
+
52
+ process.on('message', async (msg) => {
53
+ if (!msg.event) return
54
+
55
+ try {
56
+ if (msg.event === 'outbound.load_pid_queue') {
57
+ await exports.load_pid_queue(msg.data)
58
+ return
59
+ }
60
+ if (msg.event === 'outbound.flush_queue') {
61
+ await exports.flush_queue(msg.domain, process.pid)
62
+ return
63
+ }
64
+ if (msg.event === 'outbound.shutdown') {
65
+ logger.info(exports, 'Shutting down temp fail queue')
66
+ temp_fail_queue.shutdown()
67
+ return
68
+ }
69
+ // ignores the message
70
+ } catch (err) {
71
+ logger.error(exports, err)
72
+ return
73
+ }
74
+ })
75
+
76
+ exports.send_email = function (from, to, contents, next, options = {}) {
77
+ const dot_stuffed = options.dot_stuffed ?? false
78
+ const notes = options.notes ?? null
79
+ const origin = options.origin ?? exports
80
+
81
+ logger.info('Sending email via params', origin)
82
+
83
+ const transaction = trans.createTransaction(null, smtp_ini)
84
+
85
+ logger.info(`Created transaction: ${transaction.uuid}`, origin)
86
+
87
+ // Adding notes passed as parameter
88
+ if (notes) transaction.notes = notes
89
+
90
+ // set MAIL FROM address, and parse if it's not an Address object
91
+ if (from instanceof Address) {
92
+ transaction.mail_from = from
93
+ } else {
94
+ try {
95
+ from = new Address(from)
96
+ } catch (err) {
97
+ return next(constants.deny, `Malformed from: ${err}`)
98
+ }
99
+ transaction.mail_from = from
100
+ }
101
+
102
+ // Make sure to is an array
103
+ if (!Array.isArray(to)) to = [to]
104
+
105
+ if (to.length === 0) {
106
+ return next(constants.deny, 'No recipients for email')
107
+ }
108
+
109
+ // Set RCPT TO's, and parse each if it's not an Address object.
110
+ for (let i = 0, l = to.length; i < l; i++) {
111
+ if (!(to[i] instanceof Address)) {
112
+ try {
113
+ to[i] = new Address(to[i])
114
+ } catch (err) {
115
+ return next(constants.deny, `Malformed to address (${to[i]}): ${err}`)
116
+ }
117
+ }
118
+ }
119
+
120
+ transaction.rcpt_to = to
121
+
122
+ // Set data_lines to lines in contents
123
+ if (typeof contents == 'string') {
124
+ let match
125
+ while ((match = utils.line_regexp.exec(contents))) {
126
+ let line = match[1]
127
+ line = line.replace(/\r?\n?$/, '\r\n') // make sure it ends in \r\n
128
+ if (dot_stuffed === false && line.length >= 3 && line.substring(0, 1) === '.') {
129
+ line = `.${line}`
130
+ }
131
+ transaction.add_data(Buffer.from(line))
132
+ contents = contents.substring(match[1].length)
133
+ if (contents.length === 0) {
134
+ break
135
+ }
136
+ }
137
+ } else {
138
+ // Assume a stream
139
+ return stream_line_reader(contents, transaction, (err) => {
140
+ if (err) {
141
+ return next(constants.denysoft, `Error from stream line reader: ${err}`)
142
+ }
143
+ exports.send_trans_email(transaction, next)
144
+ })
145
+ }
146
+
147
+ transaction.message_stream.add_line_end()
148
+
149
+ // Allow for the removal of Message-Id and/or Date headers which
150
+ // is useful when resending mail from a quarantine.
151
+ if (options.remove_msgid) {
152
+ transaction.remove_header('Message-Id')
153
+ }
154
+ if (options.remove_date) {
155
+ transaction.remove_header('Date')
156
+ }
157
+
158
+ this.send_trans_email(transaction, next)
159
+ }
160
+
161
+ function stream_line_reader(stream, transaction, cb) {
162
+ let current_data = ''
163
+ function process_data(data) {
164
+ current_data += data.toString()
165
+ let results
166
+ while ((results = utils.line_regexp.exec(current_data))) {
167
+ const this_line = results[1]
168
+ current_data = current_data.slice(this_line.length)
169
+ if (!(current_data.length || this_line.length)) {
170
+ return
171
+ }
172
+ transaction.add_data(Buffer.from(this_line))
173
+ }
174
+ }
175
+
176
+ function process_end() {
177
+ if (current_data.length) {
178
+ transaction.add_data(Buffer.from(current_data))
179
+ }
180
+ current_data = ''
181
+ transaction.message_stream.add_line_end()
182
+ cb()
183
+ }
184
+
185
+ stream.on('data', process_data)
186
+ stream.once('end', process_end)
187
+ stream.once('error', cb)
188
+ }
189
+
190
+ function get_deliveries(transaction) {
191
+ const deliveries = []
192
+
193
+ if (obc.cfg.always_split) {
194
+ logger.debug(exports, 'always split')
195
+ for (const rcpt of transaction.rcpt_to) {
196
+ deliveries.push({ domain: rcpt.host, rcpts: [rcpt] })
197
+ }
198
+ return deliveries
199
+ }
200
+
201
+ // First get each domain
202
+ const recips = {}
203
+ for (const rcpt of transaction.rcpt_to) {
204
+ const domain = rcpt.host
205
+ if (!recips[domain]) {
206
+ recips[domain] = []
207
+ }
208
+ recips[domain].push(rcpt)
209
+ }
210
+ for (const domain of Object.keys(recips)) {
211
+ deliveries.push({ domain, rcpts: recips[domain] })
212
+ }
213
+ return deliveries
214
+ }
215
+
216
+ exports.send_trans_email = function (transaction, next) {
217
+ // add potentially missing headers
218
+ if (!transaction.header.get_all('Message-Id').length) {
219
+ logger.info(exports, 'Adding missing Message-Id header')
220
+ transaction.add_header('Message-Id', `<${transaction.uuid}@${net_utils.get_primary_host_name()}>`)
221
+ }
222
+ if (transaction.header.get('Message-Id') === '<>') {
223
+ logger.info(exports, 'Replacing empty Message-Id header')
224
+ transaction.remove_header('Message-Id')
225
+ transaction.add_header('Message-Id', `<${transaction.uuid}@${net_utils.get_primary_host_name()}>`)
226
+ }
227
+ if (!transaction.header.get_all('Date').length) {
228
+ logger.info(exports, 'Adding missing Date header')
229
+ transaction.add_header('Date', utils.date_to_str(new Date()))
230
+ }
231
+
232
+ if (obc.cfg.received_header !== 'disabled') {
233
+ transaction.add_leading_header('Received', `(${obc.cfg.received_header}); ${utils.date_to_str(new Date())}`)
234
+ }
235
+
236
+ const connection = { transaction }
237
+
238
+ logger.add_log_methods(connection)
239
+ if (!transaction.results) {
240
+ logger.debug(exports, 'adding results store')
241
+ transaction.results = new ResultStore(connection)
242
+ }
243
+
244
+ connection.pre_send_trans_email_respond = async () => {
245
+ const deliveries = get_deliveries(transaction)
246
+ const hmails = []
247
+ const ok_paths = []
248
+
249
+ let todo_index = 1
250
+
251
+ // See haraka/Haraka#3551
252
+ await new Promise((resolve) => setImmediate(resolve))
253
+
254
+ try {
255
+ for (const deliv of deliveries) {
256
+ const todo = new TODOItem(deliv.domain, deliv.rcpts, transaction)
257
+ todo.uuid = `${todo.uuid}.${todo_index}`
258
+ todo_index++
259
+ await this.process_delivery(ok_paths, todo, hmails)
260
+ }
261
+ } catch (err) {
262
+ for (let i = 0, l = ok_paths.length; i < l; i++) {
263
+ await fs.unlink(ok_paths[i]).catch(() => {})
264
+ }
265
+ transaction.results.add({ name: 'outbound' }, { err })
266
+ if (next) next(constants.denysoft, err)
267
+ return
268
+ }
269
+
270
+ for (const hmail of hmails) {
271
+ delivery_queue.push(hmail)
272
+ }
273
+
274
+ transaction.results.add({ name: 'outbound' }, { pass: 'queued' })
275
+ if (next) next(constants.ok, `Message Queued (${transaction.uuid})`)
276
+ }
277
+
278
+ plugins.run_hooks('pre_send_trans_email', connection)
279
+ }
280
+
281
+ exports.process_delivery = function (ok_paths, todo, hmails) {
282
+ return new Promise((resolve, reject) => {
283
+ logger.info(exports, `Transaction delivery for domain: ${todo.domain}`)
284
+ const fname = _qfile.name()
285
+ const tmp_path = path.join(queue_dir, `${_qfile.platformDOT}${fname}`)
286
+ const ws = new FsyncWriteStream(tmp_path, {
287
+ flags: constants.WRITE_EXCL,
288
+ })
289
+
290
+ ws.on('close', async () => {
291
+ const dest_path = path.join(queue_dir, fname)
292
+ try {
293
+ await fs.rename(tmp_path, dest_path)
294
+ hmails.push(new HMailItem(fname, dest_path, todo.notes))
295
+ ok_paths.push(dest_path)
296
+ resolve()
297
+ } catch (err) {
298
+ logger.error(exports, `Unable to rename tmp file: ${err}`)
299
+ await fs.unlink(tmp_path).catch(() => {})
300
+ reject('Queue error')
301
+ }
302
+ })
303
+
304
+ ws.on('error', async (err) => {
305
+ logger.error(exports, `Unable to write queue file (${fname}): ${err}`)
306
+ ws.destroy()
307
+ await fs.unlink(tmp_path).catch(() => {})
308
+ reject('Queueing failed')
309
+ })
310
+
311
+ this.build_todo(todo, ws, () => {
312
+ todo.message_stream.pipe(ws, {
313
+ dot_stuffed: false,
314
+ })
315
+ })
316
+ })
317
+ }
318
+
319
+ exports.build_todo = (todo, ws, write_more) => {
320
+ const todo_str = `\n${JSON.stringify(todo, exclude_from_json, '\t')}\n`
321
+ const todo_len = Buffer.byteLength(todo_str)
322
+
323
+ const buf = Buffer.alloc(4 + todo_len)
324
+ buf.writeUInt32BE(todo_len, 0)
325
+ buf.write(todo_str, 4)
326
+
327
+ const continue_writing = ws.write(buf)
328
+ if (continue_writing) {
329
+ process.nextTick(write_more)
330
+ return
331
+ }
332
+
333
+ ws.once('drain', write_more)
334
+ }
335
+
336
+ // Replacer function to exclude items from the queue file header
337
+ function exclude_from_json(key, value) {
338
+ switch (key) {
339
+ case 'message_stream':
340
+ return undefined
341
+ default:
342
+ return value
343
+ }
344
+ }
345
+
346
+ // exported for testability
347
+ exports.TODOItem = TODOItem
348
+
349
+ exports.HMailItem = HMailItem
@@ -0,0 +1,93 @@
1
+ 'use strict'
2
+
3
+ const os = require('node:os')
4
+ const platform_dot = `${['win32', 'win64'].includes(process.platform) ? '' : '__tmp__'}.`
5
+
6
+ let QFILECOUNTER = 0
7
+
8
+ const _qfile = (module.exports = {
9
+ // File Name Format: $arrival_$nextattempt_$attempts_$pid_$uniquetag_$counter_$host
10
+ hostname: (hostname) => {
11
+ if (!hostname) hostname = os.hostname()
12
+ return hostname.replace(/\\/g, '\\057').replace(/:/g, '\\072').replace(/_/g, '\\137')
13
+ },
14
+
15
+ name(overrides) {
16
+ const o = overrides || {}
17
+ const time = _qfile.time()
18
+ return [
19
+ o.arrival || time,
20
+ o.next_attempt || time,
21
+ o.attempts || 0,
22
+ o.pid || process.pid,
23
+ o.uid || _qfile.rnd_unique(),
24
+ _qfile.next_counter(),
25
+ this.hostname(o.host),
26
+ ].join('_')
27
+ },
28
+
29
+ time: () => new Date().getTime(),
30
+
31
+ next_counter: () => {
32
+ QFILECOUNTER = QFILECOUNTER < 10000 ? QFILECOUNTER + 1 : 0
33
+ return QFILECOUNTER
34
+ },
35
+
36
+ rnd_unique: (len = 6) => {
37
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
38
+ const result = []
39
+ for (let i = len; i > 0; --i) {
40
+ result.push(chars[Math.floor(Math.random() * chars.length)])
41
+ }
42
+ return result.join('')
43
+ },
44
+
45
+ parts: (filename) => {
46
+ if (!filename) throw new Error('No filename provided')
47
+
48
+ const PARTS_EXPECTED_OLD = 4
49
+ const PARTS_EXPECTED_CURRENT = 7
50
+ let p = filename.split('_')
51
+
52
+ // bail on unknown split lengths
53
+ switch (p.length) {
54
+ case PARTS_EXPECTED_OLD:
55
+ case PARTS_EXPECTED_CURRENT:
56
+ break
57
+ default:
58
+ return null
59
+ }
60
+
61
+ const time = _qfile.time()
62
+ if (p.length === PARTS_EXPECTED_OLD) {
63
+ // parse the previous string structure
64
+ // $nextattempt_$attempts_$pid_$uniq.$host
65
+ // 1484878079415_0_12345_8888.mta1.example.com
66
+ // var fn_re = /^(\d+)_(\d+)_(\d+)(_\d+\..*)$/
67
+ // match[1] = $nextattempt
68
+ // match[2] = $attempts
69
+ // match[3] = $pid
70
+ // match[4] = $uniq.$my_hostname
71
+ const fn_re = /^(\d+)_(\d+)_(\d+)_(\d+)\.(.*)$/
72
+ const match = filename.match(fn_re)
73
+ if (!match) return null
74
+
75
+ p = match.slice(1) // grab the capture groups minus the pattern
76
+ p.splice(3, 1, _qfile.rnd_unique(), _qfile.next_counter()) // add a fresh UID and counter
77
+ p.unshift(time) // prepend current timestamp -- potentially inaccurate, but non-critical and shortlived
78
+ }
79
+
80
+ return {
81
+ arrival: parseInt(p[0]),
82
+ next_attempt: parseInt(p[1]),
83
+ attempts: parseInt(p[2]),
84
+ pid: parseInt(p[3]),
85
+ uid: p[4],
86
+ counter: parseInt(p[5]),
87
+ host: p[6],
88
+ age: time - parseInt(p[0]),
89
+ }
90
+ },
91
+
92
+ platformDOT: platform_dot,
93
+ })