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,1504 @@
1
+ 'use strict'
2
+
3
+ const events = require('node:events')
4
+ const fs = require('node:fs/promises')
5
+ const { createReadStream } = require('node:fs')
6
+ const dns = require('node:dns')
7
+ const net = require('node:net')
8
+ const path = require('node:path')
9
+
10
+ const { Address } = require('../address')
11
+ const config = require('haraka-config')
12
+ const constants = require('haraka-constants')
13
+ const DSN = require('haraka-dsn')
14
+ const message = require('haraka-email-message')
15
+ const net_utils = require('haraka-net-utils')
16
+ const Notes = require('haraka-notes')
17
+ const utils = require('haraka-utils')
18
+
19
+ const logger = require('../logger')
20
+ const plugins = require('../plugins')
21
+
22
+ const client_pool = require('./client_pool')
23
+ const _qfile = require('./qfile')
24
+ const outbound = require('./index')
25
+ const obtls = require('./tls')
26
+
27
+ const FsyncWriteStream = utils.FsyncWriteStream
28
+
29
+ let queue_dir
30
+ let temp_fail_queue
31
+ let delivery_queue
32
+ setImmediate(() => {
33
+ const queuelib = require('./queue')
34
+ queue_dir = queuelib.queue_dir
35
+ temp_fail_queue = queuelib.temp_fail_queue
36
+ delivery_queue = queuelib.delivery_queue
37
+ })
38
+
39
+ const obc = require('./config')
40
+
41
+ /////////////////////////////////////////////////////////////////////////////
42
+ // HMailItem - encapsulates an individual outbound mail item
43
+
44
+ function dummy_func() {}
45
+
46
+ class HMailItem extends events.EventEmitter {
47
+ constructor(filename, filePath, notes) {
48
+ super()
49
+
50
+ const parts = _qfile.parts(filename)
51
+ if (!parts) throw new Error(`Bad filename: ${filename}`)
52
+
53
+ this.cfg = obc.cfg
54
+ this.obtls = obtls
55
+ this.name = 'outbound'
56
+ this.path = filePath
57
+ this.filename = filename
58
+ this.next_process = parts.next_attempt
59
+ this.num_failures = parts.attempts
60
+ this.pid = parts.pid
61
+ this.notes = notes || new Notes()
62
+ this.refcount = 1
63
+ this.todo = null
64
+ this.file_size = 0
65
+ this.next_cb = dummy_func
66
+ this.bounce_error = null
67
+ this.hook = null
68
+ this.size_file()
69
+ }
70
+
71
+ data_stream() {
72
+ return createReadStream(this.path, {
73
+ start: this.data_start,
74
+ end: this.file_size,
75
+ })
76
+ }
77
+
78
+ async size_file() {
79
+ try {
80
+ const stats = await fs.stat(this.path)
81
+ if (stats.size === 0) {
82
+ this.logerror(`Error reading queue file ${this.filename}: zero bytes`)
83
+ this.emit('error', `Error reading queue file ${this.filename}: zero bytes`)
84
+ return
85
+ }
86
+
87
+ this.file_size = stats.size
88
+ this.read_todo()
89
+ } catch (err) {
90
+ // The file is unreadable (deleted, permissions, I/O error) and this.todo
91
+ // is still null, so there is no sender to bounce to. Release the queue slot.
92
+ this.logerror(`Error obtaining file size: ${err}`)
93
+ this.next_cb()
94
+ }
95
+ }
96
+
97
+ async read_todo() {
98
+ try {
99
+ const bytes = await this._stream_bytes_from(this.path, { start: 0, end: 3 })
100
+
101
+ const todo_len = bytes.readUInt32BE(0)
102
+ this.logdebug(`todo header length: ${todo_len}`)
103
+ this.data_start = todo_len + 4
104
+
105
+ const todo_bytes = await this._stream_bytes_from(this.path, { start: 4, end: todo_len + 3 })
106
+ if (todo_bytes.length !== todo_len) {
107
+ const wrongLength = `Didn't find right amount of data in todo: ${this.path}`
108
+ this.logcrit(wrongLength)
109
+ try {
110
+ await fs.rename(this.path, path.join(queue_dir, `error.${this.filename}`))
111
+ } catch (renameErr) {
112
+ this.logerror(`Failed to move corrupt todo file ${this.path} to error queue: ${renameErr}`)
113
+ }
114
+ this.emit('error', wrongLength) // Note nothing picks this up yet
115
+ return
116
+ }
117
+
118
+ // we read everything
119
+ const todo_json = todo_bytes.toString().trim()
120
+ const last_char = todo_json.charAt(todo_json.length - 1)
121
+ if (last_char !== '}') {
122
+ this.emit('error', `invalid todo header end char: ${last_char} at pos ${todo_len} of ${this.filename}`)
123
+ return
124
+ }
125
+ this.todo = JSON.parse(todo_json)
126
+ this.todo.mail_from = new Address(this.todo.mail_from)
127
+ this.todo.rcpt_to = this.todo.rcpt_to.map((a) => new Address(a))
128
+ this.todo.notes = new Notes(this.todo.notes)
129
+ this.emit('ready')
130
+ } catch (err) {
131
+ const errMsg = `Error reading queue file ${this.filename}: ${err}`
132
+ this.logerror(errMsg)
133
+ this.temp_fail(errMsg)
134
+ }
135
+ }
136
+
137
+ _stream_bytes_from(file_path, opts) {
138
+ return new Promise((resolve, reject) => {
139
+ if (opts.encoding !== undefined) {
140
+ // passing an encoding to fs.createReadStream will change the type of data returned
141
+ // ex: instead of returning a buffer, it may return a String, which will cause
142
+ // Buffer.concat to barf. There's a reason this function has 'bytes' in the name
143
+ reject(new Error('Thar be dragons here! Encode/decode on the result of this function'))
144
+ return
145
+ }
146
+ const stream = createReadStream(file_path, opts)
147
+ stream.on('error', reject)
148
+
149
+ let raw_bytes = Buffer.alloc(0)
150
+ stream.on('data', (data) => {
151
+ raw_bytes = Buffer.concat([raw_bytes, data])
152
+ })
153
+
154
+ stream.on('end', () => {
155
+ resolve(raw_bytes)
156
+ })
157
+ })
158
+ }
159
+
160
+ send() {
161
+ if (obc.cfg.disabled) {
162
+ // try again in 1 second if delivery is disabled
163
+ this.logdebug('delivery disabled temporarily. Retrying in 1s.')
164
+ setTimeout(() => {
165
+ this.send()
166
+ }, 1000)
167
+ return
168
+ }
169
+
170
+ if (!this.todo) {
171
+ this.once('ready', () => {
172
+ this._send()
173
+ })
174
+ } else {
175
+ this._send()
176
+ }
177
+ }
178
+
179
+ _send() {
180
+ plugins.run_hooks('send_email', this)
181
+ }
182
+
183
+ send_email_respond(retval, delay_seconds) {
184
+ if (retval === constants.delay) {
185
+ // Try again in 'delay' seconds.
186
+ this.logdebug(`Delivery of this email delayed for ${delay_seconds} seconds`)
187
+ this.next_cb()
188
+ temp_fail_queue.add(this.filename, delay_seconds * 1000, () => {
189
+ delivery_queue.push(this)
190
+ })
191
+ } else {
192
+ this.logdebug(`Sending mail: ${this.filename}`)
193
+ this.get_mx()
194
+ }
195
+ }
196
+
197
+ get_mx() {
198
+ const { domain } = this.todo
199
+ plugins.run_hooks('get_mx', this, domain)
200
+ }
201
+
202
+ async get_mx_respond(retval, mx) {
203
+ switch (retval) {
204
+ case constants.ok: {
205
+ this.logdebug(`MX from Plugin: ${this.todo.domain} => 0 ${JSON.stringify(mx)}`)
206
+ let mx_list
207
+ if (Array.isArray(mx)) {
208
+ mx_list = mx.map((m) => new net_utils.HarakaMx(m))
209
+ } else {
210
+ mx_list = [new net_utils.HarakaMx(mx)]
211
+ }
212
+ return this.found_mx(mx_list)
213
+ }
214
+ case constants.deny:
215
+ this.logwarn(`get_mx plugin returned DENY: ${mx}`)
216
+ for (const rcpt of this.todo.rcpt_to) {
217
+ this.extend_rcpt_with_dsn(rcpt, DSN.addr_bad_dest_system(`No MX for ${this.todo.domain}`))
218
+ }
219
+ return this.bounce(`No MX for ${this.todo.domain}`)
220
+ case constants.denysoft:
221
+ this.logwarn(`get_mx plugin returned DENYSOFT: ${mx}`)
222
+ for (const rcpt of this.todo.rcpt_to) {
223
+ this.extend_rcpt_with_dsn(
224
+ rcpt,
225
+ DSN.addr_bad_dest_system(`Temporary MX lookup error for ${this.todo.domain}`, 450),
226
+ )
227
+ }
228
+ return this.temp_fail(`Temporary MX lookup error for ${this.todo.domain}`)
229
+ }
230
+
231
+ // none of the above return codes, drop through to DNS
232
+ try {
233
+ const exchanges = await net_utils.get_mx(this.todo.domain)
234
+
235
+ if (exchanges.length) {
236
+ this.found_mx(this.sort_mx(exchanges))
237
+ } else {
238
+ for (const rcpt of this.todo.rcpt_to) {
239
+ this.extend_rcpt_with_dsn(
240
+ rcpt,
241
+ DSN.addr_bad_dest_system(`Nowhere to deliver mail to for domain: ${this.todo.domain}`),
242
+ )
243
+ }
244
+ this.bounce(`Nowhere to deliver mail to for domain: ${this.todo.domain}`)
245
+ }
246
+ } catch (e) {
247
+ this.get_mx_error(e)
248
+ }
249
+ }
250
+
251
+ get_mx_error(err) {
252
+ this.lognotice(`MX Lookup for ${this.todo.domain} failed: ${err}`)
253
+
254
+ if (err.code === dns.NXDOMAIN || err.code === dns.NOTFOUND) {
255
+ for (const rcpt of this.todo.rcpt_to) {
256
+ this.extend_rcpt_with_dsn(rcpt, DSN.addr_bad_dest_system(`No Such Domain: ${this.todo.domain}`))
257
+ }
258
+ this.bounce(`No Such Domain: ${this.todo.domain}`)
259
+ } else {
260
+ // every other error is transient
261
+ for (const rcpt of this.todo.rcpt_to) {
262
+ this.extend_rcpt_with_dsn(rcpt, DSN.addr_unspecified(`DNS lookup failure: ${this.todo.domain}`))
263
+ }
264
+ this.temp_fail(`DNS lookup failure: ${err}`)
265
+ }
266
+ }
267
+
268
+ async found_mx(mxs) {
269
+ // support RFC 7505 null MX
270
+ if (mxs.length === 1 && mxs[0].priority === 0 && mxs[0].exchange === '') {
271
+ for (const rcpt of this.todo.rcpt_to) {
272
+ this.extend_rcpt_with_dsn(
273
+ rcpt,
274
+ DSN.addr_null_mx(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`),
275
+ )
276
+ }
277
+ return this.bounce(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`)
278
+ }
279
+
280
+ // resolves the MX hostnames to IPs
281
+ this.mxlist = await net_utils.resolve_mx_hosts(mxs)
282
+
283
+ switch (obc.cfg.inet_prefer) {
284
+ case 'v4':
285
+ this.mxlist = [
286
+ ...this.mxlist.filter((mx) => !net.isIP(mx.exchange) || net.isIPv4(mx.exchange)),
287
+ ...this.mxlist.filter((mx) => net.isIPv6(mx.exchange)),
288
+ ]
289
+ break
290
+ case 'v6':
291
+ this.mxlist = [
292
+ ...this.mxlist.filter((mx) => !net.isIP(mx.exchange) || net.isIPv6(mx.exchange)),
293
+ ...this.mxlist.filter((mx) => net.isIPv4(mx.exchange)),
294
+ ]
295
+ break
296
+ }
297
+
298
+ this.try_deliver()
299
+ }
300
+
301
+ async try_deliver() {
302
+ // are any MXs left?
303
+ if (this.mxlist.length === 0) {
304
+ for (const rcpt of this.todo.rcpt_to) {
305
+ this.extend_rcpt_with_dsn(rcpt, DSN.addr_bad_dest_system(`Tried all MXs ${this.todo.domain}`))
306
+ }
307
+ return this.temp_fail('Tried all MXs')
308
+ }
309
+
310
+ const mx = this.mxlist.shift()
311
+
312
+ if (!obc.cfg.local_mx_ok && mx.from_dns && (await net_utils.is_local_host(mx.exchange))) {
313
+ this.loginfo(`MX ${mx.exchange} is local, skipping since local_mx_ok=false`)
314
+ return this.try_deliver() // try next MX
315
+ }
316
+
317
+ this.force_tls = this.get_force_tls(mx)
318
+
319
+ if (this.todo.notes.outbound_ip) {
320
+ this.logerror(`notes.outbound_ip is deprecated. Use get_mx.bind instead!`)
321
+ if (!mx.bind) mx.bind = this.todo.notes.outbound_ip
322
+ }
323
+
324
+ // Allow transaction notes to set outbound IP helo
325
+ if (this.todo.notes.outbound_helo) {
326
+ mx.bind_helo = this.todo.notes.outbound_helo
327
+ }
328
+
329
+ const host = mx.path ? mx.path : mx.exchange
330
+ const lmtp = mx.using_lmtp ? ' using LMTP' : ''
331
+ if (!mx.port) mx.port = mx.using_lmtp ? 24 : 25
332
+ const from_dns = mx.from_dns ? ' (via DNS)' : ''
333
+
334
+ this.logdebug(
335
+ `deliver: ${mx.bind_helo} -> ${host}${lmtp}${from_dns} (${delivery_queue.length()}) (${temp_fail_queue.length()})`,
336
+ )
337
+ client_pool.get_client(mx, (err, socket) => {
338
+ if (err) {
339
+ if (/connection timed out|connect ECONNREFUSED/.test(err)) {
340
+ logger.notice(this, `Failed to get socket: ${err}`)
341
+ } else {
342
+ logger.error(this, `Failed to get socket: ${err}`)
343
+ }
344
+
345
+ return this.try_deliver() // try next MX
346
+ }
347
+ this.try_deliver_host_on_socket(mx, host, mx.port, socket)
348
+ })
349
+ }
350
+
351
+ try_deliver_host_on_socket(mx, host, port, socket) {
352
+ const self = this
353
+ let processing_mail = true
354
+ let command = mx.using_lmtp ? 'connect_lmtp' : 'connect'
355
+
356
+ for (const l of ['error', 'timeout', 'close', 'end']) {
357
+ socket.removeAllListeners(l)
358
+ }
359
+
360
+ socket.once('timeout', function () {
361
+ socket.emit('error', `socket timeout waiting on ${command}`)
362
+ })
363
+
364
+ socket.on('error', (err) => {
365
+ if (!processing_mail) return
366
+
367
+ self.logerror(`Ongoing connection failed to ${host}:${port} : ${err}`)
368
+ processing_mail = false
369
+ client_pool.release_client(socket, mx)
370
+ if (err.source === 'tls')
371
+ // exception thrown from tls_socket during tls upgrade
372
+ return obtls.mark_tls_nogo(host, () => {
373
+ return self.try_deliver()
374
+ })
375
+ self.try_deliver() // try the next MX
376
+ })
377
+
378
+ socket.once('close', () => {
379
+ if (!processing_mail) return
380
+
381
+ self.logerror(`Remote end ${host}:${port} closed connection while we were processing mail. Trying next MX.`)
382
+ processing_mail = false
383
+ client_pool.release_client(socket, mx)
384
+ self.try_deliver()
385
+ })
386
+
387
+ socket.once('end', () => {
388
+ socket.writable = false
389
+ if (!processing_mail) client_pool.release_client(socket, mx)
390
+ })
391
+
392
+ let response = []
393
+
394
+ let recip_index = 0
395
+ const recipients = this.todo.rcpt_to
396
+ let lmtp_rcpt_idx = 0
397
+
398
+ let last_recip = null
399
+ const ok_recips = []
400
+ const fail_recips = []
401
+ const bounce_recips = []
402
+ let secured = false
403
+ let authenticating = false
404
+ let authenticated = false
405
+ let smtp_properties = {
406
+ tls: false,
407
+ max_size: 0,
408
+ eightbitmime: false,
409
+ enh_status_codes: false,
410
+ auth: [],
411
+ }
412
+
413
+ const send_command = (socket.send_command = (cmd, data) => {
414
+ if (!socket.writable) {
415
+ self.logerror('Socket writability went away')
416
+ if (processing_mail) {
417
+ processing_mail = false
418
+ client_pool.release_client(socket, mx)
419
+ return self.try_deliver()
420
+ }
421
+ return
422
+ }
423
+ if (self.force_tls && !['EHLO', 'LHLO', 'STARTTLS'].includes(cmd.toUpperCase()) && !socket.isSecure()) {
424
+ // For safety against programming mistakes
425
+ self.logerror(
426
+ 'Blocking attempt to send unencrypted data to forced TLS socket. This message indicates a programming error in the software.',
427
+ )
428
+ processing_mail = false
429
+ client_pool.release_client(socket, mx)
430
+ return
431
+ }
432
+
433
+ let line = `${cmd}${data ? ` ${data}` : ''}`
434
+ if (cmd === 'dot' || cmd === 'dot_lmtp') {
435
+ line = '.'
436
+ }
437
+ if (authenticating) cmd = 'auth'
438
+ self.logprotocol(`C: ${line}`)
439
+ socket.write(`${line}\r\n`, 'utf8', (err) => {
440
+ if (err) {
441
+ self.logcrit(`Socket write failed unexpectedly: ${err}`)
442
+ // We may want to release client here - but I want to get this
443
+ // line of code in before we do that so we might see some logging
444
+ // in case of errors.
445
+ // client_pool.release_client(socket, mx);
446
+ }
447
+ })
448
+ command = cmd.toLowerCase()
449
+ response = []
450
+ })
451
+
452
+ function set_ehlo_props() {
453
+ for (let i = 0, l = response.length; i < l; i++) {
454
+ const r = response[i]
455
+ if (r.toUpperCase() === '8BITMIME') {
456
+ smtp_properties.eightbitmime = true
457
+ } else if (r.toUpperCase() === 'STARTTLS') {
458
+ smtp_properties.tls = true
459
+ } else if (r.toUpperCase() === 'ENHANCEDSTATUSCODES') {
460
+ smtp_properties.enh_status_codes = true
461
+ } else if (r.toUpperCase() === 'SMTPUTF8') {
462
+ smtp_properties.smtputf8 = true
463
+ } else {
464
+ // Check for SIZE parameter and limit
465
+ let matches = r.match(/^SIZE\s+(\d+)$/)
466
+ if (matches) {
467
+ smtp_properties.max_size = matches[1]
468
+ }
469
+ // Check for AUTH
470
+ matches = r.match(/^AUTH\s+(.+)$/)
471
+ if (matches) {
472
+ smtp_properties.auth = matches[1].split(/\s+/)
473
+ }
474
+ }
475
+ }
476
+ }
477
+
478
+ function get_reverse_path_with_params() {
479
+ const rp = self.todo.mail_from.format(!smtp_properties.smtputf8)
480
+ let rp_params = ''
481
+ if (smtp_properties.smtputf8 && has_non_ascii(rp)) rp_params += ' SMTPUTF8'
482
+ return `FROM:${rp}${rp_params}`
483
+ }
484
+
485
+ function has_non_ascii(string) {
486
+ return [...string].some((char) => char.charCodeAt(0) > 127)
487
+ }
488
+
489
+ function auth_and_mail_phase() {
490
+ if (!authenticated && mx.auth_user && mx.auth_pass) {
491
+ // We have AUTH credentials to send for this domain
492
+
493
+ if (!(Array.isArray(smtp_properties.auth) && smtp_properties.auth.length)) {
494
+ // AUTH not offered
495
+ self.logwarn(
496
+ `AUTH configured for domain ${self.todo.domain} but host ${host} did not advertise AUTH capability`,
497
+ )
498
+ // Try and send the message without authentication
499
+ return send_command('MAIL', get_reverse_path_with_params())
500
+ }
501
+
502
+ if (!mx.auth_type) {
503
+ // User hasn't specified an authentication type, so we pick one
504
+ // We'll prefer CRAM-MD5 as it's the most secure that we support.
505
+ if (smtp_properties.auth.includes('CRAM-MD5')) {
506
+ mx.auth_type = 'CRAM-MD5'
507
+ }
508
+ // PLAIN requires less round-trips compared to LOGIN
509
+ else if (smtp_properties.auth.includes('PLAIN')) {
510
+ // PLAIN requires less round trips compared to LOGIN
511
+ // So we'll make this our 2nd pick.
512
+ mx.auth_type = 'PLAIN'
513
+ } else if (smtp_properties.auth.includes('LOGIN')) {
514
+ mx.auth_type = 'LOGIN'
515
+ }
516
+ }
517
+
518
+ if (!mx.auth_type || (mx.auth_type && !smtp_properties.auth.includes(mx.auth_type.toUpperCase()))) {
519
+ // No compatible authentication types offered by the server
520
+ self.logwarn(
521
+ `AUTH configured for domain ${self.todo.domain} but host ${host}did not offer any compatible types${mx.auth_type ? ` (requested: ${mx.auth_type})` : ''} (offered: ${smtp_properties.auth.join(',')})`,
522
+ )
523
+ // Proceed without authentication
524
+ return send_command('MAIL', get_reverse_path_with_params())
525
+ }
526
+
527
+ switch (mx.auth_type.toUpperCase()) {
528
+ case 'PLAIN':
529
+ return send_command('AUTH', `PLAIN ${utils.base64(`\0${mx.auth_user}\0${mx.auth_pass}`)}`)
530
+ case 'LOGIN':
531
+ authenticating = true
532
+ return send_command('AUTH', 'LOGIN')
533
+ case 'CRAM-MD5':
534
+ authenticating = true
535
+ return send_command('AUTH', 'CRAM-MD5')
536
+ default:
537
+ // Unsupported AUTH type
538
+ self.logwarn(
539
+ `Unsupported authentication type ${mx.auth_type.toUpperCase()} requested for domain ${self.todo.domain}`,
540
+ )
541
+ return send_command('MAIL', get_reverse_path_with_params())
542
+ }
543
+ }
544
+
545
+ return send_command('MAIL', get_reverse_path_with_params())
546
+ }
547
+
548
+ // IMPORTANT: do STARTTLS before AUTH for security
549
+ function process_ehlo_data() {
550
+ set_ehlo_props()
551
+
552
+ if (secured) return auth_and_mail_phase() // TLS already negotiated
553
+
554
+ if (self.force_tls) {
555
+ self.logdebug(`Using TLS for domain: ${self.todo.domain}, host: ${host}`)
556
+
557
+ if (!obc.cfg.enable_tls || !smtp_properties.tls) {
558
+ // Prevent further use of the non-securable socket
559
+ processing_mail = false
560
+ socket.write('QUIT\r\n', 'utf8') // courtesy
561
+ socket.end()
562
+ client_pool.release_client(socket, mx)
563
+ return self.temp_fail(`No TLS available but required by configuration.`)
564
+ }
565
+
566
+ socket.once('secure', () => {
567
+ // Set this flag so we don't try STARTTLS again if it
568
+ // is incorrectly offered at EHLO once we are secured.
569
+ secured = true
570
+ send_command(mx.using_lmtp ? 'LHLO' : 'EHLO', mx.bind_helo)
571
+ })
572
+ return send_command('STARTTLS')
573
+ }
574
+ if (!obc.cfg.enable_tls) return auth_and_mail_phase() // TLS not enabled
575
+ if (!smtp_properties.tls) return auth_and_mail_phase() // TLS not advertised by remote
576
+
577
+ if (obtls.cfg === undefined) {
578
+ self.logerror(`Oops, TLS config not loaded yet!`)
579
+ return auth_and_mail_phase() // no outbound TLS config
580
+ }
581
+
582
+ // TLS is configured and available
583
+
584
+ // TLS exclude lists checks for MX host or remote domain
585
+ if (net_utils.ip_in_list(obtls.cfg.no_tls_hosts, host)) return auth_and_mail_phase()
586
+ if (net_utils.ip_in_list(obtls.cfg.no_tls_hosts, self.todo.domain)) return auth_and_mail_phase()
587
+
588
+ // Check Redis and skip for hosts that failed past TLS upgrade
589
+ return obtls.check_tls_nogo(
590
+ host,
591
+ () => {
592
+ // Clear to GO
593
+ self.logdebug(`Trying TLS for domain: ${self.todo.domain}, host: ${host}`)
594
+
595
+ socket.once('secure', () => {
596
+ // Set this flag so we don't try STARTTLS again if it
597
+ // is incorrectly offered at EHLO once we are secured.
598
+ secured = true
599
+ send_command(mx.using_lmtp ? 'LHLO' : 'EHLO', mx.bind_helo)
600
+ })
601
+ return send_command('STARTTLS')
602
+ },
603
+ (when) => {
604
+ // No GO
605
+ self.loginfo(`TLS disabled for ${host} because it was marked as non-TLS on ${when}`)
606
+ return auth_and_mail_phase()
607
+ },
608
+ )
609
+ }
610
+
611
+ let fp_called = false
612
+
613
+ function finish_processing_mail(success) {
614
+ if (fp_called) {
615
+ return self.logerror(`finish_processing_mail called multiple times! Stack: ${new Error().stack}`)
616
+ }
617
+ fp_called = true
618
+ if (fail_recips.length) {
619
+ self.refcount++
620
+ self.split_to_new_recipients(fail_recips, 'Some recipients temporarily failed', (hmail) => {
621
+ self.discard()
622
+ hmail.temp_fail(`Some recipients temp failed: ${fail_recips.join(', ')}`, {
623
+ fail_recips,
624
+ mx,
625
+ })
626
+ })
627
+ }
628
+ if (bounce_recips.length) {
629
+ self.refcount++
630
+ self.split_to_new_recipients(bounce_recips, 'Some recipients rejected', (hmail) => {
631
+ self.discard()
632
+ hmail.bounce(`Some recipients failed: ${bounce_recips.join(', ')}`, {
633
+ bounce_recips,
634
+ mx,
635
+ })
636
+ })
637
+ }
638
+ processing_mail = false
639
+ if (success) {
640
+ const reason = response.join(' ')
641
+
642
+ let hostname = mx.exchange
643
+ if (net.isIP(hostname) && mx.from_dns && !net.isIP(mx.from_dns)) {
644
+ hostname = mx.from_dns
645
+ }
646
+
647
+ self.delivered(
648
+ host,
649
+ port,
650
+ mx.using_lmtp ? 'LMTP' : 'SMTP',
651
+ hostname,
652
+ reason,
653
+ ok_recips,
654
+ fail_recips,
655
+ bounce_recips,
656
+ secured,
657
+ authenticated,
658
+ )
659
+ } else {
660
+ self.discard()
661
+ }
662
+
663
+ send_command('QUIT')
664
+ }
665
+
666
+ socket.on('line', (line) => {
667
+ if (!processing_mail && command !== 'rset') {
668
+ if (command !== 'quit') {
669
+ self.logprotocol(`Received data after stopping processing: ${line}`)
670
+ }
671
+ return
672
+ }
673
+ self.logprotocol(`S: ${line}`)
674
+ const matches = smtp_regexp.exec(line)
675
+ if (!matches) {
676
+ // Unrecognized response.
677
+ self.logerror(`Unrecognized response from upstream server: ${line}`)
678
+ processing_mail = false
679
+ // Release back to the pool and instruct it to terminate this connection
680
+ client_pool.release_client(socket, mx)
681
+ for (const rcpt of self.todo.rcpt_to) {
682
+ self.extend_rcpt_with_dsn(
683
+ rcpt,
684
+ DSN.proto_invalid_command(`Unrecognized response from upstream server: ${line}`),
685
+ )
686
+ }
687
+ self.bounce(`Unrecognized response from upstream server: ${line}`, { mx })
688
+ return
689
+ }
690
+
691
+ let reason
692
+ const code = matches[1]
693
+ const cont = matches[2]
694
+ const extc = matches[3]
695
+ const rest = matches[4]
696
+ response.push(rest)
697
+ if (cont !== ' ') return
698
+
699
+ if (code.match(/^2/)) {
700
+ // Successful command, fall through
701
+ } else if (code.match(/^3/) && command !== 'data') {
702
+ if (authenticating) {
703
+ const resp = response.join(' ')
704
+ switch (mx.auth_type.toUpperCase()) {
705
+ case 'LOGIN':
706
+ if (resp === 'VXNlcm5hbWU6') {
707
+ // Username:
708
+ return send_command(utils.base64(mx.auth_user))
709
+ } else if (resp === 'UGFzc3dvcmQ6') {
710
+ // Password:
711
+ return send_command(utils.base64(mx.auth_pass))
712
+ }
713
+ break
714
+ case 'CRAM-MD5':
715
+ // The response is our challenge
716
+ return send_command(utils.cram_md5_response(mx.auth_user, mx.auth_pass, resp))
717
+ default:
718
+ // This shouldn't happen...
719
+ }
720
+ }
721
+ // Error
722
+ reason = response.join(' ')
723
+ for (const rcpt of recipients) {
724
+ rcpt.dsn_action = 'delayed'
725
+ rcpt.dsn_smtp_code = code
726
+ rcpt.dsn_smtp_extc = extc
727
+ rcpt.dsn_status = extc
728
+ rcpt.dsn_smtp_response = response.join(' ')
729
+ rcpt.dsn_remote_mta = mx.exchange
730
+ }
731
+ send_command('QUIT')
732
+ processing_mail = false
733
+ return self.temp_fail(`Upstream error: ${code} ${extc ? `${extc} ` : ''}${reason}`)
734
+ } else if (code.match(/^4/)) {
735
+ authenticating = false
736
+ if (/^rcpt/.test(command) || command === 'dot_lmtp') {
737
+ if (command === 'dot_lmtp') last_recip = ok_recips.shift()
738
+ // this recipient was rejected
739
+ reason = `${code} ${extc ? `${extc} ` : ''}${response.join(' ')}`
740
+ self.lognotice(`recipient ${last_recip} deferred: ${reason}`)
741
+ last_recip.reason = reason
742
+
743
+ last_recip.dsn_action = 'delayed'
744
+ last_recip.dsn_smtp_code = code
745
+ last_recip.dsn_smtp_extc = extc
746
+ last_recip.dsn_status = extc
747
+ last_recip.dsn_smtp_response = response.join(' ')
748
+ last_recip.dsn_remote_mta = mx.exchange
749
+
750
+ fail_recips.push(last_recip)
751
+ if (command === 'dot_lmtp') {
752
+ response = []
753
+ if (ok_recips.length === 0) {
754
+ return finish_processing_mail(false)
755
+ }
756
+ }
757
+ } else if (processing_mail) {
758
+ reason = response.join(' ')
759
+ for (const rcpt of recipients) {
760
+ rcpt.dsn_action = 'delayed'
761
+ rcpt.dsn_smtp_code = code
762
+ rcpt.dsn_smtp_extc = extc
763
+ rcpt.dsn_status = extc
764
+ rcpt.dsn_smtp_response = response.join(' ')
765
+ rcpt.dsn_remote_mta = mx.exchange
766
+ }
767
+ send_command('QUIT')
768
+ processing_mail = false
769
+ return self.temp_fail(`Upstream error: ${code} ${extc ? `${extc} ` : ''}${reason}`)
770
+ } else {
771
+ reason = response.join(' ')
772
+ self.lognotice(`Error - but not processing mail: ${code} ${extc ? `${extc} ` : ''}${reason}`)
773
+ return client_pool.release_client(socket, mx)
774
+ }
775
+ } else if (code.match(/^5/)) {
776
+ authenticating = false
777
+ if (command === 'ehlo') {
778
+ // EHLO command was rejected; fall-back to HELO
779
+ return send_command('HELO', mx.bind_helo)
780
+ }
781
+ if (command === 'rset') {
782
+ // Broken server doesn't accept RSET, terminate the connection
783
+ return client_pool.release_client(socket, mx)
784
+ }
785
+ reason = `${code} ${extc ? `${extc} ` : ''}${response.join(' ')}`
786
+ if (/^rcpt/.test(command) || command === 'dot_lmtp') {
787
+ if (command === 'dot_lmtp') last_recip = ok_recips.shift()
788
+ self.lognotice(`recipient ${last_recip} rejected: ${reason}`)
789
+ last_recip.reason = reason
790
+
791
+ last_recip.dsn_action = 'failed'
792
+ last_recip.dsn_smtp_code = code
793
+ last_recip.dsn_smtp_extc = extc
794
+ last_recip.dsn_status = extc
795
+ last_recip.dsn_smtp_response = response.join(' ')
796
+ last_recip.dsn_remote_mta = mx.exchange
797
+
798
+ bounce_recips.push(last_recip)
799
+ if (command === 'dot_lmtp') {
800
+ response = []
801
+ if (ok_recips.length === 0) {
802
+ return finish_processing_mail(false)
803
+ }
804
+ }
805
+ } else {
806
+ for (const rcpt of recipients) {
807
+ rcpt.dsn_action = 'failed'
808
+ rcpt.dsn_smtp_code = code
809
+ rcpt.dsn_smtp_extc = extc
810
+ rcpt.dsn_status = extc
811
+ rcpt.dsn_smtp_response = response.join(' ')
812
+ rcpt.dsn_remote_mta = mx.exchange
813
+ }
814
+ send_command('QUIT')
815
+ processing_mail = false
816
+ return self.bounce(reason, { mx })
817
+ }
818
+ }
819
+
820
+ switch (command) {
821
+ case 'connect':
822
+ send_command('EHLO', mx.bind_helo)
823
+ break
824
+ case 'connect_lmtp':
825
+ send_command('LHLO', mx.bind_helo)
826
+ break
827
+ case 'lhlo':
828
+ case 'ehlo':
829
+ process_ehlo_data()
830
+ break
831
+ case 'starttls': {
832
+ const tls_options = obtls.get_tls_options(mx)
833
+ if (self.force_tls) tls_options.rejectUnauthorized = true
834
+
835
+ smtp_properties = {}
836
+ socket.upgrade(tls_options, (authorized, verifyError, cert, cipher) => {
837
+ const loginfo = {
838
+ verified: authorized,
839
+ }
840
+ if (cipher) {
841
+ loginfo.cipher = cipher.name
842
+ loginfo.version = cipher.version
843
+ }
844
+ if (verifyError) loginfo.error = verifyError
845
+ if (cert?.subject) {
846
+ loginfo.cn = cert.subject.CN
847
+ loginfo.organization = cert.subject.O
848
+ }
849
+ if (cert?.issuer) loginfo.issuer = cert.issuer.O
850
+ if (cert?.valid_to) loginfo.expires = cert.valid_to
851
+ if (cert?.fingerprint) loginfo.fingerprint = cert.fingerprint
852
+ self.loginfo('secured', loginfo)
853
+
854
+ if (self.force_tls && !authorized) {
855
+ processing_mail = false
856
+ socket.end()
857
+ self.temp_fail('Host failed TLS verification required by configuration.')
858
+ client_pool.release_client(socket, mx)
859
+ }
860
+ })
861
+ break
862
+ }
863
+ case 'auth':
864
+ authenticating = false
865
+ authenticated = true
866
+ send_command('MAIL', get_reverse_path_with_params())
867
+ break
868
+ case 'helo':
869
+ send_command('MAIL', get_reverse_path_with_params())
870
+ break
871
+ case 'mail':
872
+ last_recip = recipients[recip_index]
873
+ recip_index++
874
+ send_command('RCPT', `TO:${last_recip.format(!smtp_properties.smtputf8)}`)
875
+ break
876
+ case 'rcpt':
877
+ if (last_recip && code.match(/^250/)) {
878
+ ok_recips.push(last_recip)
879
+ }
880
+ if (recip_index === recipients.length) {
881
+ // End of RCPT TOs
882
+ if (ok_recips.length > 0) {
883
+ send_command('DATA')
884
+ } else {
885
+ finish_processing_mail(false)
886
+ }
887
+ } else {
888
+ last_recip = recipients[recip_index]
889
+ recip_index++
890
+ send_command('RCPT', `TO:${last_recip.format(!smtp_properties.smtputf8)}`)
891
+ }
892
+ break
893
+ case 'data': {
894
+ const data_stream = self.data_stream()
895
+ data_stream.on('data', (data) => {
896
+ self.logdata(`C: ${data}`)
897
+ })
898
+ data_stream.on('error', (err) => {
899
+ self.logerror(`Reading from the data stream failed: ${err}`)
900
+ })
901
+ data_stream.on('end', () => {
902
+ send_command(mx.using_lmtp ? 'dot_lmtp' : 'dot')
903
+ })
904
+ data_stream.pipe(socket, { end: false })
905
+ break
906
+ }
907
+ case 'dot':
908
+ finish_processing_mail(true)
909
+ break
910
+ case 'dot_lmtp':
911
+ if (code.match(/^2/)) lmtp_rcpt_idx++
912
+ if (lmtp_rcpt_idx === ok_recips.length) {
913
+ finish_processing_mail(true)
914
+ }
915
+ break
916
+ case 'quit':
917
+ case 'rset':
918
+ client_pool.release_client(socket, mx)
919
+ break
920
+ default:
921
+ // should never get here - means we did something
922
+ // wrong.
923
+ throw new Error(`Unknown command: ${command}`)
924
+ }
925
+ })
926
+
927
+ if (socket.__fromPool) {
928
+ logger.debug(this, 'got socket, trying to deliver')
929
+ secured = socket.isEncrypted()
930
+ logger.debug(this, `got ${secured ? 'TLS ' : ''}socket, trying to deliver`)
931
+ send_command('MAIL', get_reverse_path_with_params())
932
+ }
933
+ }
934
+
935
+ extend_rcpt_with_dsn(rcpt, dsn) {
936
+ rcpt.dsn_code = dsn.code
937
+ rcpt.dsn_msg = dsn.msg
938
+ rcpt.dsn_status = `${dsn.cls}.${dsn.sub}.${dsn.det}`
939
+ if (dsn.cls == 4) {
940
+ rcpt.dsn_action = 'delayed'
941
+ } else if (dsn.cls == 5) {
942
+ rcpt.dsn_action = 'failed'
943
+ }
944
+ }
945
+
946
+ populate_bounce_message(from, to, reason, cb) {
947
+ let buf = ''
948
+ const original_header_lines = []
949
+ let headers_done = false
950
+ const header = new message.Header()
951
+
952
+ try {
953
+ const data_stream = this.data_stream()
954
+ data_stream.on('data', (data) => {
955
+ if (headers_done === false) {
956
+ buf += data
957
+ let results
958
+ while ((results = utils.line_regexp.exec(buf))) {
959
+ const this_line = results[1]
960
+ if (this_line === '\n' || this_line == '\r\n') {
961
+ headers_done = true
962
+ break
963
+ }
964
+ buf = buf.slice(this_line.length)
965
+ original_header_lines.push(this_line)
966
+ }
967
+ }
968
+ })
969
+ data_stream.on('end', () => {
970
+ if (original_header_lines.length > 0) {
971
+ header.parse(original_header_lines)
972
+ }
973
+ this.populate_bounce_message_with_headers(from, to, reason, header, cb)
974
+ })
975
+ data_stream.on('error', (err) => {
976
+ cb(err)
977
+ })
978
+ } catch {
979
+ this.populate_bounce_message_with_headers(from, to, reason, header, cb)
980
+ }
981
+ }
982
+
983
+ /**
984
+ * Generates a bounce message
985
+ *
986
+ * hmail.todo.rcpt_to objects should be extended as follows:
987
+ * - dsn_action
988
+ * - dsn_status
989
+ * - dsn_code
990
+ * - dsn_msg
991
+ *
992
+ * - dsn_remote_mta
993
+ *
994
+ * Upstream code/message goes here:
995
+ * - dsn_smtp_code
996
+ * - dsn_smtp_extc
997
+ * - dsn_smtp_response
998
+ *
999
+ * @param from
1000
+ * @param to
1001
+ * @param reason
1002
+ * @param header
1003
+ * @param cb - a callback for fn(err, message_body_lines)
1004
+ */
1005
+ populate_bounce_message_with_headers(from, to, reason, header, cb) {
1006
+ const CRLF = '\r\n'
1007
+
1008
+ const originalMessageId = header.get('Message-Id')
1009
+
1010
+ const bounce_msg_ = config.get('outbound.bounce_message', 'data')
1011
+ const bounce_msg_html_ = config.get('outbound.bounce_message_html', 'data')
1012
+ const bounce_msg_image_ = config.get('outbound.bounce_message_image', 'data')
1013
+
1014
+ const bounce_header_lines = []
1015
+ const bounce_body_lines = []
1016
+ const bounce_html_lines = []
1017
+ const bounce_image_lines = []
1018
+ let bounce_headers_done = false
1019
+
1020
+ const values = {
1021
+ date: utils.date_to_str(new Date()),
1022
+ me: net_utils.get_primary_host_name(),
1023
+ from,
1024
+ to,
1025
+ subject: header.get_decoded('Subject').trim(),
1026
+ recipients: this.todo.rcpt_to.join(', '),
1027
+ reason,
1028
+ extended_reason: this.todo.rcpt_to
1029
+ .map((recip) => {
1030
+ if (recip.reason) {
1031
+ return `${recip.original}: ${recip.reason}`
1032
+ }
1033
+ })
1034
+ .join('\n'),
1035
+ pid: process.pid,
1036
+ msgid: `<${utils.uuid()}@${net_utils.get_primary_host_name()}>`,
1037
+ }
1038
+
1039
+ for (let line of bounce_msg_) {
1040
+ line = line.replace(/\{(\w+)\}/g, (i, word) => values[word] || '?')
1041
+
1042
+ if (bounce_headers_done == false && line == '') {
1043
+ bounce_headers_done = true
1044
+ } else if (bounce_headers_done == false) {
1045
+ bounce_header_lines.push(line)
1046
+ } else if (bounce_headers_done == true) {
1047
+ bounce_body_lines.push(line)
1048
+ }
1049
+ }
1050
+
1051
+ const escaped_chars = {
1052
+ '&': 'amp',
1053
+ '<': 'lt',
1054
+ '>': 'gt',
1055
+ '"': 'quot',
1056
+ "'": 'apos',
1057
+ '\r': '#10',
1058
+ '\n': '#13',
1059
+ }
1060
+ const escape_pattern = new RegExp(`[${Object.keys(escaped_chars).join('')}]`, 'g')
1061
+
1062
+ for (let line of bounce_msg_html_) {
1063
+ line = line.replace(/\{(\w+)\}/g, (i, word) => {
1064
+ if (word in values) {
1065
+ return String(values[word]).replace(escape_pattern, (m) => `&${escaped_chars[m]};`)
1066
+ } else {
1067
+ return '?'
1068
+ }
1069
+ })
1070
+
1071
+ bounce_html_lines.push(line)
1072
+ }
1073
+
1074
+ for (const line of bounce_msg_image_) {
1075
+ bounce_image_lines.push(line)
1076
+ }
1077
+
1078
+ const boundary = `boundary_${utils.uuid()}`
1079
+ const bounce_body = []
1080
+
1081
+ for (const line of bounce_header_lines) {
1082
+ bounce_body.push(`${line}${CRLF}`)
1083
+ }
1084
+ bounce_body.push(
1085
+ `Content-Type: multipart/report; report-type=delivery-status;${CRLF} boundary="${boundary}"${CRLF}`,
1086
+ )
1087
+ // Adding references to original msg id
1088
+ if (originalMessageId != '') {
1089
+ bounce_body.push(`References: ${originalMessageId.replace(/(\r?\n)*$/, '')}${CRLF}`)
1090
+ }
1091
+
1092
+ bounce_body.push(CRLF)
1093
+ bounce_body.push(`This is a MIME-encapsulated message.${CRLF}`)
1094
+ bounce_body.push(CRLF)
1095
+
1096
+ let boundary_incr = ''
1097
+ if (bounce_html_lines.length > 1) {
1098
+ boundary_incr = 'a'
1099
+ bounce_body.push(`--${boundary}${CRLF}`)
1100
+ bounce_body.push(`Content-Type: multipart/related; boundary="${boundary}${boundary_incr}"${CRLF}`)
1101
+ bounce_body.push(CRLF)
1102
+ bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
1103
+ boundary_incr = 'b'
1104
+ bounce_body.push(`Content-Type: multipart/alternative; boundary="${boundary}${boundary_incr}"${CRLF}`)
1105
+ bounce_body.push(CRLF)
1106
+ }
1107
+
1108
+ bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
1109
+ bounce_body.push(`Content-Type: text/plain; charset=us-ascii${CRLF}`)
1110
+ bounce_body.push(CRLF)
1111
+ for (const line of bounce_body_lines) {
1112
+ bounce_body.push(`${line}${CRLF}`)
1113
+ }
1114
+ bounce_body.push(CRLF)
1115
+
1116
+ if (bounce_html_lines.length > 1) {
1117
+ bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
1118
+ bounce_body.push(`Content-Type: text/html; charset=us-ascii${CRLF}`)
1119
+ bounce_body.push(CRLF)
1120
+ for (const line of bounce_html_lines) {
1121
+ bounce_body.push(`${line}${CRLF}`)
1122
+ }
1123
+ bounce_body.push(CRLF)
1124
+ bounce_body.push(`--${boundary}${boundary_incr}--${CRLF}`)
1125
+
1126
+ if (bounce_image_lines.length > 1) {
1127
+ boundary_incr = 'a'
1128
+ bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
1129
+ //bounce_body.push(`Content-Type: text/html; charset=us-ascii${CRLF}`);
1130
+ //bounce_body.push(CRLF);
1131
+ for (const line of bounce_image_lines) {
1132
+ bounce_body.push(`${line}${CRLF}`)
1133
+ }
1134
+ bounce_body.push(CRLF)
1135
+ bounce_body.push(`--${boundary}${boundary_incr}--${CRLF}`)
1136
+ }
1137
+ }
1138
+
1139
+ bounce_body.push(`--${boundary}${CRLF}`)
1140
+ bounce_body.push(`Content-type: message/delivery-status${CRLF}`)
1141
+ bounce_body.push(CRLF)
1142
+ if (originalMessageId != '') {
1143
+ bounce_body.push(`Original-Envelope-Id: ${originalMessageId.replace(/(\r?\n)*$/, '')}${CRLF}`)
1144
+ }
1145
+ bounce_body.push(`Reporting-MTA: dns;${net_utils.get_primary_host_name()}${CRLF}`)
1146
+ if (this.todo.queue_time) {
1147
+ bounce_body.push(`Arrival-Date: ${utils.date_to_str(new Date(this.todo.queue_time))}${CRLF}`)
1148
+ }
1149
+ for (const rcpt_to of this.todo.rcpt_to) {
1150
+ bounce_body.push(CRLF)
1151
+ bounce_body.push(`Final-Recipient: rfc822;${rcpt_to.address}${CRLF}`)
1152
+ let dsn_action = null
1153
+ if (rcpt_to.dsn_action) {
1154
+ dsn_action = rcpt_to.dsn_action
1155
+ } else if (rcpt_to.dsn_code) {
1156
+ if (/^5/.exec(rcpt_to.dsn_code)) {
1157
+ dsn_action = 'failed'
1158
+ } else if (/^4/.exec(rcpt_to.dsn_code)) {
1159
+ dsn_action = 'delayed'
1160
+ } else if (/^2/.exec(rcpt_to.dsn_code)) {
1161
+ dsn_action = 'delivered'
1162
+ }
1163
+ } else if (rcpt_to.dsn_smtp_code) {
1164
+ if (/^5/.exec(rcpt_to.dsn_smtp_code)) {
1165
+ dsn_action = 'failed'
1166
+ } else if (/^4/.exec(rcpt_to.dsn_smtp_code)) {
1167
+ dsn_action = 'delayed'
1168
+ } else if (/^2/.exec(rcpt_to.dsn_smtp_code)) {
1169
+ dsn_action = 'delivered'
1170
+ }
1171
+ }
1172
+ if (dsn_action != null) {
1173
+ bounce_body.push(`Action: ${dsn_action}${CRLF}`)
1174
+ }
1175
+ if (rcpt_to.dsn_status) {
1176
+ let { dsn_status } = rcpt_to
1177
+ if (rcpt_to.dsn_code || rcpt_to.dsn_msg) {
1178
+ dsn_status += ' ('
1179
+ if (rcpt_to.dsn_code) {
1180
+ dsn_status += rcpt_to.dsn_code
1181
+ }
1182
+ if (rcpt_to.dsn_code || rcpt_to.dsn_msg) {
1183
+ dsn_status += ' '
1184
+ }
1185
+ if (rcpt_to.dsn_msg) {
1186
+ dsn_status += rcpt_to.dsn_msg
1187
+ }
1188
+ dsn_status += ')'
1189
+ }
1190
+ bounce_body.push(`Status: ${dsn_status}${CRLF}`)
1191
+ }
1192
+ if (rcpt_to.dsn_remote_mta) {
1193
+ bounce_body.push(`Remote-MTA: ${rcpt_to.dsn_remote_mta}${CRLF}`)
1194
+ }
1195
+ let diag_code = null
1196
+ if (rcpt_to.dsn_smtp_code || rcpt_to.dsn_smtp_extc || rcpt_to.dsn_smtp_response) {
1197
+ diag_code = 'smtp;'
1198
+ if (rcpt_to.dsn_smtp_code) {
1199
+ diag_code += `${rcpt_to.dsn_smtp_code} `
1200
+ }
1201
+ if (rcpt_to.dsn_smtp_extc) {
1202
+ diag_code += `${rcpt_to.dsn_smtp_extc} `
1203
+ }
1204
+ if (rcpt_to.dsn_smtp_response) {
1205
+ diag_code += `${rcpt_to.dsn_smtp_response} `
1206
+ }
1207
+ }
1208
+ if (diag_code != null) {
1209
+ bounce_body.push(`Diagnostic-Code: ${diag_code}${CRLF}`)
1210
+ }
1211
+ }
1212
+ bounce_body.push(CRLF)
1213
+
1214
+ bounce_body.push(`--${boundary}${CRLF}`)
1215
+ bounce_body.push(`Content-Description: Undelivered Message Headers${CRLF}`)
1216
+ bounce_body.push(`Content-Type: text/rfc822-headers${CRLF}`)
1217
+ bounce_body.push(CRLF)
1218
+ for (const line of header.header_list) {
1219
+ bounce_body.push(line)
1220
+ }
1221
+ bounce_body.push(CRLF)
1222
+
1223
+ bounce_body.push(`--${boundary}--${CRLF}`)
1224
+
1225
+ cb(null, bounce_body)
1226
+ }
1227
+
1228
+ bounce(err, opts) {
1229
+ this.loginfo(`bouncing mail: ${err}`)
1230
+ if (!this.todo) {
1231
+ this.once('ready', () => {
1232
+ this._bounce(err, opts)
1233
+ })
1234
+ return
1235
+ }
1236
+ this._bounce(err, opts)
1237
+ }
1238
+
1239
+ _bounce(err, opts) {
1240
+ err = new Error(err)
1241
+ if (opts) {
1242
+ err.mx = opts.mx
1243
+ err.deferred_rcpt = opts.fail_recips
1244
+ err.bounced_rcpt = opts.bounce_recips
1245
+ }
1246
+ this.bounce_error = err
1247
+ plugins.run_hooks('bounce', this, err)
1248
+ }
1249
+
1250
+ bounce_respond(retval) {
1251
+ if (retval !== constants.cont) {
1252
+ this.loginfo(`Plugin responded with: ${retval}. Not sending bounce.`)
1253
+ return this.discard() // calls next_cb
1254
+ }
1255
+
1256
+ const self = this
1257
+ const err = this.bounce_error
1258
+
1259
+ if (!this.todo.mail_from.user) {
1260
+ // double bounce - mail was already a bounce
1261
+ return this.double_bounce('Mail was already a bounce')
1262
+ }
1263
+
1264
+ const from = new Address('<>')
1265
+ const recip = new Address(this.todo.mail_from.user, this.todo.mail_from.host)
1266
+ this.populate_bounce_message(from, recip, err, function (err2, data_lines) {
1267
+ if (err2) {
1268
+ return self.double_bounce(`Error populating bounce message: ${err2}`)
1269
+ }
1270
+
1271
+ outbound.send_email(
1272
+ from,
1273
+ recip,
1274
+ data_lines.join(''),
1275
+ (code) => {
1276
+ if (code === constants.deny) {
1277
+ // failed to even queue the mail
1278
+ return self.double_bounce('Unable to queue the bounce message. Not sending bounce!')
1279
+ }
1280
+ self.discard()
1281
+ },
1282
+ { origin: this },
1283
+ )
1284
+ })
1285
+ }
1286
+
1287
+ double_bounce(err) {
1288
+ this.lognotice(`Double bounce: ${err}`)
1289
+ fs.unlink(this.path).catch(() => {})
1290
+ this.next_cb()
1291
+ // TODO: fill this in... ?
1292
+ // One strategy is perhaps log to an mbox file. What do other servers do?
1293
+ // Another strategy might be delivery "plugins" to cope with this.
1294
+ }
1295
+
1296
+ delivered(ip, port, mode, host, response, ok_recips, fail_recips, bounce_recips, secured, authenticated) {
1297
+ const delay = (Date.now() - this.todo.queue_time) / 1000
1298
+ this.lognotice({
1299
+ 'delivered file': this.filename,
1300
+ domain: this.todo.domain,
1301
+ host,
1302
+ ip,
1303
+ port,
1304
+ mode,
1305
+ tls: secured ? 'Y' : 'N',
1306
+ auth: authenticated ? 'Y' : 'N',
1307
+ response,
1308
+ delay,
1309
+ fails: this.num_failures,
1310
+ rcpts: `${ok_recips.length}/${fail_recips.length}/${bounce_recips.length}`,
1311
+ })
1312
+ plugins.run_hooks('delivered', this, [host, ip, response, delay, port, mode, ok_recips, secured, authenticated])
1313
+ }
1314
+
1315
+ discard() {
1316
+ this.refcount--
1317
+ if (this.refcount === 0) {
1318
+ // Remove the file.
1319
+ fs.unlink(this.path).catch(() => {})
1320
+ this.next_cb()
1321
+ }
1322
+ }
1323
+
1324
+ convert_temp_failed_to_bounce(err, extra) {
1325
+ for (const rcpt_to of this.todo.rcpt_to) {
1326
+ rcpt_to.dsn_action = 'failed'
1327
+ if (rcpt_to.dsn_status) {
1328
+ rcpt_to.dsn_status = `${rcpt_to.dsn_status}`.replace(/^4/, '5')
1329
+ }
1330
+ }
1331
+ return this.bounce(err, extra)
1332
+ }
1333
+
1334
+ temp_fail(err, extra) {
1335
+ logger.debug(this, `Temp fail for: ${err}`)
1336
+ this.num_failures++
1337
+
1338
+ // Test for max failures which is configurable.
1339
+ if (this.num_failures > obc.cfg.temp_fail_intervals.length) {
1340
+ return this.convert_temp_failed_to_bounce(`Too many failures (${err})`, extra)
1341
+ }
1342
+
1343
+ const delay = obc.cfg.temp_fail_intervals[this.num_failures - 1]
1344
+
1345
+ plugins.run_hooks('deferred', this, { delay, err, ...(extra || {}) })
1346
+ }
1347
+
1348
+ async deferred_respond(retval, msg, params) {
1349
+ if (retval !== constants.cont && retval !== constants.denysoft) {
1350
+ this.loginfo(`plugin responded with: ${retval}. Not deferring. Deleting mail.`)
1351
+ return this.discard() // calls next_cb
1352
+ }
1353
+
1354
+ let delay = params.delay * 1000
1355
+
1356
+ if (retval === constants.denysoft) {
1357
+ delay = parseInt(msg, 10) * 1000
1358
+ }
1359
+
1360
+ this.loginfo(`Temp failing ${this.filename} for ${delay / 1000} seconds: ${params.err}`)
1361
+ const parts = _qfile.parts(this.filename)
1362
+ parts.next_attempt = Date.now() + delay
1363
+ parts.attempts = this.num_failures
1364
+ const new_filename = _qfile.name(parts)
1365
+
1366
+ try {
1367
+ await fs.rename(this.path, path.join(queue_dir, new_filename))
1368
+ this.path = path.join(queue_dir, new_filename)
1369
+ this.filename = new_filename
1370
+
1371
+ this.next_cb()
1372
+
1373
+ temp_fail_queue.add(this.filename, delay, () => {
1374
+ delivery_queue.push(this)
1375
+ })
1376
+ } catch (err) {
1377
+ return this.bounce(`Error re-queueing email: ${err}`)
1378
+ }
1379
+ }
1380
+
1381
+ // The following handler impacts outgoing mail. It removes the queue file.
1382
+ delivered_respond(retval, msg) {
1383
+ if (retval !== constants.cont && retval !== constants.ok) {
1384
+ this.logwarn('delivered plugin responded', { retval, msg })
1385
+ }
1386
+ this.discard()
1387
+ }
1388
+
1389
+ get_force_tls(mx) {
1390
+ if (!mx.exchange) return false
1391
+ if (!obtls.cfg.force_tls_hosts) return false
1392
+
1393
+ if (net_utils.ip_in_list(obtls.cfg.force_tls_hosts, mx.exchange)) {
1394
+ this.logdebug(`Forcing TLS for host ${mx.exchange}`)
1395
+ return true
1396
+ }
1397
+
1398
+ if (mx.from_dns) {
1399
+ // the MX was looked up in DNS and already resolved to IP(s).
1400
+ // This checks the hostname.
1401
+ if (net_utils.ip_in_list(obtls.cfg.force_tls_hosts, mx.from_dns)) {
1402
+ this.logdebug(`Forcing TLS for host ${mx.from_dns}`)
1403
+ return true
1404
+ }
1405
+ }
1406
+
1407
+ if (net_utils.ip_in_list(obtls.cfg.force_tls_hosts, this.todo.domain)) {
1408
+ this.logdebug(`Forcing TLS for domain ${this.todo.domain}`)
1409
+ return true
1410
+ }
1411
+
1412
+ return false
1413
+ }
1414
+
1415
+ sort_mx(mx_list) {
1416
+ // MXs must be sorted by priority.
1417
+ const sorted = mx_list.sort((a, b) => a.priority - b.priority)
1418
+
1419
+ // Matched priorities must be randomly shuffled.
1420
+ // This isn't a very good shuffle but it'll do for now.
1421
+ for (let i = 0, l = sorted.length - 1; i < l; i++) {
1422
+ if (sorted[i].priority === sorted[i + 1].priority) {
1423
+ if (Math.round(Math.random())) {
1424
+ // 0 or 1
1425
+ const j = sorted[i]
1426
+ sorted[i] = sorted[i + 1]
1427
+ sorted[i + 1] = j
1428
+ }
1429
+ }
1430
+ }
1431
+
1432
+ return sorted
1433
+ }
1434
+
1435
+ split_to_new_recipients(recipients, response, cb) {
1436
+ const hmail = this
1437
+ if (recipients.length === hmail.todo.rcpt_to.length) {
1438
+ // Split to new for no reason - increase refcount and return self
1439
+ hmail.refcount++
1440
+ return cb(hmail)
1441
+ }
1442
+ const fname = _qfile.name()
1443
+ const tmp_path = path.join(queue_dir, `${_qfile.platformDOT}${fname}`)
1444
+ const ws = new FsyncWriteStream(tmp_path, {
1445
+ flags: constants.WRITE_EXCL,
1446
+ })
1447
+ function err_handler(err, location) {
1448
+ logger.error(this, `Error while splitting to new recipients (${location}): ${err}`)
1449
+ for (const rcpt of hmail.todo.rcpt_to) {
1450
+ hmail.extend_rcpt_with_dsn(rcpt, DSN.sys_unspecified(`Error splitting to new recipients: ${err}`))
1451
+ }
1452
+ hmail.bounce(`Error splitting to new recipients: ${err}`)
1453
+ }
1454
+
1455
+ ws.on('error', (err) => {
1456
+ err_handler(err, 'tmp file writer')
1457
+ })
1458
+
1459
+ let writing = false
1460
+
1461
+ function write_more() {
1462
+ if (writing) return
1463
+ writing = true
1464
+ const rs = hmail.data_stream()
1465
+ rs.pipe(ws, { end: false })
1466
+ rs.on('error', (err) => {
1467
+ err_handler(err, 'hmail.data_stream reader')
1468
+ })
1469
+ rs.on('end', async () => {
1470
+ try {
1471
+ await ws.close()
1472
+ const dest_path = path.join(queue_dir, fname)
1473
+ await fs.rename(tmp_path, dest_path)
1474
+ const split_mail = new HMailItem(fname, dest_path, hmail.notes)
1475
+ split_mail.once('ready', () => {
1476
+ cb(split_mail)
1477
+ })
1478
+ } catch (err) {
1479
+ err_handler(err, 'tmp file close/rename')
1480
+ }
1481
+ })
1482
+ }
1483
+
1484
+ ws.on('error', (err) => {
1485
+ logger.error(this, `Unable to write queue file (${fname}): ${err}`)
1486
+ ws.destroy()
1487
+ for (const rcpt of hmail.todo.rcpt_to) {
1488
+ hmail.extend_rcpt_with_dsn(rcpt, DSN.sys_unspecified(`Error re-queueing some recipients: ${err}`))
1489
+ }
1490
+ hmail.bounce(`Error re-queueing some recipients: ${err}`)
1491
+ })
1492
+
1493
+ const new_todo = JSON.parse(JSON.stringify(hmail.todo))
1494
+ new_todo.rcpt_to = recipients
1495
+ outbound.build_todo(new_todo, ws, write_more)
1496
+ }
1497
+ }
1498
+
1499
+ module.exports = HMailItem
1500
+ module.exports.obtls = obtls
1501
+
1502
+ logger.add_log_methods(HMailItem)
1503
+
1504
+ const smtp_regexp = /^([2345]\d\d)([ -])#?(?:(\d\.\d\.\d)\s)?(.*)/