haraka 0.0.33 → 3.3.1

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 (254) hide show
  1. package/.githooks/pre-commit +41 -0
  2. package/.prettierignore +7 -0
  3. package/.qlty/.gitignore +7 -0
  4. package/.qlty/configs/.shellcheckrc +1 -0
  5. package/.qlty/qlty.toml +15 -0
  6. package/CHANGELOG.md +1898 -0
  7. package/CONTRIBUTORS.md +34 -0
  8. package/Dockerfile +50 -0
  9. package/LICENSE +22 -0
  10. package/Plugins.md +227 -0
  11. package/README.md +119 -4
  12. package/SECURITY.md +178 -0
  13. package/TODO +22 -0
  14. package/bin/haraka +593 -0
  15. package/bin/haraka_grep +32 -0
  16. package/config/aliases +2 -0
  17. package/config/auth_flat_file.ini +7 -0
  18. package/config/auth_vpopmaild.ini +9 -0
  19. package/config/connection.ini +79 -0
  20. package/config/delay_deny.ini +7 -0
  21. package/config/host_list +3 -0
  22. package/config/host_list_regex +6 -0
  23. package/config/http.ini +11 -0
  24. package/config/lmtp.ini +7 -0
  25. package/config/log.ini +11 -0
  26. package/config/outbound.bounce_message +18 -0
  27. package/config/outbound.bounce_message_html +36 -0
  28. package/config/outbound.bounce_message_image +106 -0
  29. package/config/outbound.ini +24 -0
  30. package/config/plugins +67 -0
  31. package/config/smtp.ini +37 -0
  32. package/config/smtp_bridge.ini +4 -0
  33. package/config/smtp_forward.ini +31 -0
  34. package/config/smtp_proxy.ini +27 -0
  35. package/config/tarpit.timeout +1 -0
  36. package/config/tls.ini +83 -0
  37. package/config/watch.ini +12 -0
  38. package/config/xclient.hosts +2 -0
  39. package/connection.js +1865 -0
  40. package/contrib/Haraka.cf +6 -0
  41. package/contrib/Haraka.pm +35 -0
  42. package/contrib/bad_smtp_server.pl +25 -0
  43. package/contrib/bsd-rc.d/haraka +63 -0
  44. package/contrib/debian-init.d/haraka +87 -0
  45. package/contrib/haraka.init +96 -0
  46. package/contrib/haraka.service +23 -0
  47. package/contrib/plugin2npm.sh +81 -0
  48. package/contrib/ubuntu-upstart/haraka.conf +27 -0
  49. package/docs/Body.md +1 -0
  50. package/docs/Config.md +1 -0
  51. package/docs/Connection.md +153 -0
  52. package/docs/CoreConfig.md +96 -0
  53. package/docs/CustomReturnCodes.md +3 -0
  54. package/docs/HAProxy.md +62 -0
  55. package/docs/Header.md +1 -0
  56. package/docs/Logging.md +129 -0
  57. package/docs/Outbound.md +210 -0
  58. package/docs/Plugins.md +372 -0
  59. package/docs/Results.md +7 -0
  60. package/docs/Transaction.md +135 -0
  61. package/docs/Tutorial.md +183 -0
  62. package/docs/deprecated/access.md +3 -0
  63. package/docs/deprecated/backscatterer.md +9 -0
  64. package/docs/deprecated/connect.rdns_access.md +53 -0
  65. package/docs/deprecated/data.headers.md +3 -0
  66. package/docs/deprecated/data.nomsgid.md +7 -0
  67. package/docs/deprecated/data.noreceived.md +11 -0
  68. package/docs/deprecated/data.rfc5322_header_checks.md +11 -0
  69. package/docs/deprecated/dkim_sign.md +97 -0
  70. package/docs/deprecated/dkim_verify.md +28 -0
  71. package/docs/deprecated/dnsbl.md +80 -0
  72. package/docs/deprecated/dnswl.md +73 -0
  73. package/docs/deprecated/lookup_rdns.strict.md +67 -0
  74. package/docs/deprecated/mail_from.access.md +52 -0
  75. package/docs/deprecated/mail_from.blocklist.md +18 -0
  76. package/docs/deprecated/mail_from.nobounces.md +8 -0
  77. package/docs/deprecated/rcpt_to.access.md +53 -0
  78. package/docs/deprecated/rcpt_to.blocklist.md +18 -0
  79. package/docs/deprecated/rcpt_to.routes.md +3 -0
  80. package/docs/deprecated/rdns.regexp.md +30 -0
  81. package/docs/plugins/aliases.md +3 -0
  82. package/docs/plugins/auth/auth_bridge.md +34 -0
  83. package/docs/plugins/auth/auth_ldap.md +4 -0
  84. package/docs/plugins/auth/auth_proxy.md +36 -0
  85. package/docs/plugins/auth/auth_vpopmaild.md +33 -0
  86. package/docs/plugins/auth/flat_file.md +40 -0
  87. package/docs/plugins/block_me.md +18 -0
  88. package/docs/plugins/data.signatures.md +11 -0
  89. package/docs/plugins/delay_deny.md +23 -0
  90. package/docs/plugins/max_unrecognized_commands.md +6 -0
  91. package/docs/plugins/prevent_credential_leaks.md +22 -0
  92. package/docs/plugins/process_title.md +42 -0
  93. package/docs/plugins/queue/deliver.md +3 -0
  94. package/docs/plugins/queue/discard.md +32 -0
  95. package/docs/plugins/queue/lmtp.md +24 -0
  96. package/docs/plugins/queue/qmail-queue.md +16 -0
  97. package/docs/plugins/queue/quarantine.md +87 -0
  98. package/docs/plugins/queue/smtp_bridge.md +32 -0
  99. package/docs/plugins/queue/smtp_forward.md +127 -0
  100. package/docs/plugins/queue/smtp_proxy.md +68 -0
  101. package/docs/plugins/queue/test.md +7 -0
  102. package/docs/plugins/rcpt_to.in_host_list.md +34 -0
  103. package/docs/plugins/rcpt_to.max_count.md +3 -0
  104. package/docs/plugins/record_envelope_addresses.md +20 -0
  105. package/docs/plugins/relay.md +3 -0
  106. package/docs/plugins/reseed_rng.md +16 -0
  107. package/docs/plugins/status.md +41 -0
  108. package/docs/plugins/tarpit.md +50 -0
  109. package/docs/plugins/tls.md +235 -0
  110. package/docs/plugins/toobusy.md +27 -0
  111. package/docs/plugins/xclient.md +10 -0
  112. package/docs/tutorials/Migrating_from_v1_to_v2.md +96 -0
  113. package/docs/tutorials/SettingUpOutbound.md +62 -0
  114. package/eslint.config.mjs +2 -0
  115. package/haraka.js +74 -0
  116. package/haraka.sh +2 -0
  117. package/http/html/404.html +58 -0
  118. package/http/html/index.html +47 -0
  119. package/http/package.json +21 -0
  120. package/line_socket.js +24 -0
  121. package/logger.js +322 -0
  122. package/outbound/client_pool.js +59 -0
  123. package/outbound/config.js +134 -0
  124. package/outbound/hmail.js +1504 -0
  125. package/outbound/index.js +349 -0
  126. package/outbound/qfile.js +93 -0
  127. package/outbound/queue.js +399 -0
  128. package/outbound/tls.js +85 -0
  129. package/outbound/todo.js +17 -0
  130. package/package.json +100 -4
  131. package/plugins/.eslintrc.yaml +3 -0
  132. package/plugins/auth/auth_base.js +261 -0
  133. package/plugins/auth/auth_bridge.js +20 -0
  134. package/plugins/auth/auth_proxy.js +227 -0
  135. package/plugins/auth/auth_vpopmaild.js +162 -0
  136. package/plugins/auth/flat_file.js +44 -0
  137. package/plugins/block_me.js +88 -0
  138. package/plugins/data.signatures.js +30 -0
  139. package/plugins/delay_deny.js +153 -0
  140. package/plugins/prevent_credential_leaks.js +61 -0
  141. package/plugins/process_title.js +197 -0
  142. package/plugins/profile.js +11 -0
  143. package/plugins/queue/deliver.js +12 -0
  144. package/plugins/queue/discard.js +27 -0
  145. package/plugins/queue/lmtp.js +45 -0
  146. package/plugins/queue/qmail-queue.js +93 -0
  147. package/plugins/queue/quarantine.js +133 -0
  148. package/plugins/queue/smtp_bridge.js +45 -0
  149. package/plugins/queue/smtp_forward.js +371 -0
  150. package/plugins/queue/smtp_proxy.js +142 -0
  151. package/plugins/queue/test.js +15 -0
  152. package/plugins/rcpt_to.host_list_base.js +65 -0
  153. package/plugins/rcpt_to.in_host_list.js +56 -0
  154. package/plugins/record_envelope_addresses.js +17 -0
  155. package/plugins/reseed_rng.js +7 -0
  156. package/plugins/status.js +274 -0
  157. package/plugins/tarpit.js +45 -0
  158. package/plugins/tls.js +164 -0
  159. package/plugins/toobusy.js +47 -0
  160. package/plugins/xclient.js +124 -0
  161. package/plugins.js +605 -0
  162. package/run_tests +11 -0
  163. package/server.js +827 -0
  164. package/smtp_client.js +504 -0
  165. package/test/.eslintrc.yaml +11 -0
  166. package/test/config/auth_flat_file.ini +5 -0
  167. package/test/config/block_me.recipient +1 -0
  168. package/test/config/block_me.senders +1 -0
  169. package/test/config/dhparams.pem +8 -0
  170. package/test/config/host_list +2 -0
  171. package/test/config/outbound_tls_cert.pem +1 -0
  172. package/test/config/outbound_tls_key.pem +1 -0
  173. package/test/config/plugins +7 -0
  174. package/test/config/smtp.ini +11 -0
  175. package/test/config/smtp_forward.ini +30 -0
  176. package/test/config/tls/example.com/_.example.com.key +28 -0
  177. package/test/config/tls/example.com/example.com.crt +25 -0
  178. package/test/config/tls/haraka.local.pem +51 -0
  179. package/test/config/tls.ini +45 -0
  180. package/test/config/tls_cert.pem +21 -0
  181. package/test/config/tls_key.pem +28 -0
  182. package/test/connection.js +820 -0
  183. package/test/fixtures/haproxy_allowed/config/connection.ini +3 -0
  184. package/test/fixtures/haproxy_disabled/config/connection.ini +3 -0
  185. package/test/fixtures/haproxy_untrusted/config/connection.ini +3 -0
  186. package/test/fixtures/line_socket.js +21 -0
  187. package/test/fixtures/todo_qfile.txt +0 -0
  188. package/test/fixtures/util_hmailitem.js +156 -0
  189. package/test/installation/config/test-plugin-flat +1 -0
  190. package/test/installation/config/test-plugin.ini +10 -0
  191. package/test/installation/config/tls.ini +1 -0
  192. package/test/installation/node_modules/load_first/index.js +5 -0
  193. package/test/installation/node_modules/load_first/package.json +11 -0
  194. package/test/installation/node_modules/test-plugin/config/test-plugin-flat +1 -0
  195. package/test/installation/node_modules/test-plugin/config/test-plugin.ini +9 -0
  196. package/test/installation/node_modules/test-plugin/package.json +5 -0
  197. package/test/installation/node_modules/test-plugin/test-plugin.js +5 -0
  198. package/test/installation/plugins/base_plugin.js +3 -0
  199. package/test/installation/plugins/folder_plugin/index.js +3 -0
  200. package/test/installation/plugins/folder_plugin/package.json +11 -0
  201. package/test/installation/plugins/inherits.js +7 -0
  202. package/test/installation/plugins/load_first.js +3 -0
  203. package/test/installation/plugins/plugin.js +1 -0
  204. package/test/installation/plugins/tls.js +3 -0
  205. package/test/logger.js +217 -0
  206. package/test/loud/config/dhparams.pem +0 -0
  207. package/test/loud/config/tls/goobered.pem +45 -0
  208. package/test/loud/config/tls.ini +43 -0
  209. package/test/mail_specimen/base64-root-part.txt +23 -0
  210. package/test/mail_specimen/varied-fold-lengths-preserve-data.txt +283 -0
  211. package/test/outbound/bounce_net_errors.js +133 -0
  212. package/test/outbound/bounce_rfc3464.js +226 -0
  213. package/test/outbound/hmail.js +210 -0
  214. package/test/outbound/index.js +385 -0
  215. package/test/outbound/qfile.js +124 -0
  216. package/test/outbound/queue.js +325 -0
  217. package/test/plugins/auth/auth_base.js +620 -0
  218. package/test/plugins/auth/auth_bridge.js +80 -0
  219. package/test/plugins/auth/auth_vpopmaild.js +81 -0
  220. package/test/plugins/auth/flat_file.js +123 -0
  221. package/test/plugins/block_me.js +141 -0
  222. package/test/plugins/data.signatures.js +111 -0
  223. package/test/plugins/delay_deny.js +262 -0
  224. package/test/plugins/prevent_credential_leaks.js +174 -0
  225. package/test/plugins/process_title.js +141 -0
  226. package/test/plugins/queue/deliver.js +98 -0
  227. package/test/plugins/queue/discard.js +78 -0
  228. package/test/plugins/queue/lmtp.js +137 -0
  229. package/test/plugins/queue/qmail-queue.js +98 -0
  230. package/test/plugins/queue/quarantine.js +80 -0
  231. package/test/plugins/queue/smtp_bridge.js +152 -0
  232. package/test/plugins/queue/smtp_forward.js +1023 -0
  233. package/test/plugins/queue/smtp_proxy.js +138 -0
  234. package/test/plugins/rcpt_to.host_list_base.js +102 -0
  235. package/test/plugins/rcpt_to.in_host_list.js +186 -0
  236. package/test/plugins/record_envelope_addresses.js +66 -0
  237. package/test/plugins/reseed_rng.js +34 -0
  238. package/test/plugins/status.js +207 -0
  239. package/test/plugins/tarpit.js +90 -0
  240. package/test/plugins/tls.js +86 -0
  241. package/test/plugins/toobusy.js +198 -0
  242. package/test/plugins/xclient.js +119 -0
  243. package/test/plugins.js +230 -0
  244. package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_fixed +0 -0
  245. package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka +0 -0
  246. package/test/queue/1508269674999_1508269674999_0_34002_socVUF_1_haraka +0 -0
  247. package/test/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka +0 -0
  248. package/test/queue/zero-length +0 -0
  249. package/test/server.js +1012 -0
  250. package/test/smtp_client.js +1303 -0
  251. package/test/tls_socket.js +321 -0
  252. package/test/transaction.js +554 -0
  253. package/tls_socket.js +771 -0
  254. package/transaction.js +267 -0
@@ -0,0 +1,93 @@
1
+ // Queue to qmail-queue
2
+
3
+ const childproc = require('node:child_process')
4
+ const fs = require('node:fs')
5
+
6
+ exports.register = function () {
7
+ this.queue_exec = this.config.get('qmail-queue.path') || '/var/qmail/bin/qmail-queue'
8
+ if (!fs.existsSync(this.queue_exec)) {
9
+ throw new Error(`Cannot find qmail-queue binary (${this.queue_exec})`)
10
+ }
11
+
12
+ this.load_qmail_queue_ini()
13
+
14
+ if (this.cfg.main.enable_outbound) {
15
+ this.register_hook('queue_outbound', 'hook_queue')
16
+ }
17
+ }
18
+
19
+ exports.load_qmail_queue_ini = function () {
20
+ this.cfg = this.config.get(
21
+ 'qmail-queue.ini',
22
+ {
23
+ booleans: ['+main.enable_outbound'],
24
+ },
25
+ () => {
26
+ this.load_qmail_queue_ini()
27
+ },
28
+ )
29
+ }
30
+
31
+ // qmail-queue envelope: F<sender>\0 (T<rcpt>\0)* \0
32
+ // Built dynamically, sized to exactly the bytes needed.
33
+ // doesn't emit zero padding after the terminating NUL.
34
+ // encodes non-ASCII (SMTPUTF8) addresses correctly
35
+ exports.build_envelope = function (transaction) {
36
+ const NUL = Buffer.from([0])
37
+ const parts = [Buffer.from('F'), Buffer.from(transaction.mail_from.address), NUL]
38
+ for (const rcpt of transaction.rcpt_to) {
39
+ parts.push(Buffer.from('T'), Buffer.from(rcpt.address), NUL)
40
+ }
41
+ parts.push(NUL)
42
+ return Buffer.concat(parts)
43
+ }
44
+
45
+ exports.hook_queue = function (next, connection) {
46
+ const plugin = this
47
+
48
+ const txn = connection?.transaction
49
+ if (!txn) return next()
50
+
51
+ const q_wants = txn.notes.get('queue.wants')
52
+ if (q_wants && q_wants !== 'qmail-queue') return next()
53
+
54
+ const qmail_queue = childproc.spawn(
55
+ this.queue_exec, // process name
56
+ [], // arguments
57
+ { stdio: ['pipe', 'pipe', process.stderr] },
58
+ )
59
+
60
+ qmail_queue.on('exit', function finished(code) {
61
+ if (code !== 0) {
62
+ connection.logerror(plugin, `Unable to queue message to qmail-queue: ${code}`)
63
+ next()
64
+ } else {
65
+ next(OK, 'Queued!')
66
+ }
67
+ })
68
+
69
+ connection.transaction.message_stream.pipe(qmail_queue.stdin, {
70
+ line_endings: '\n',
71
+ })
72
+
73
+ qmail_queue.stdin.on('close', () => {
74
+ if (!connection?.transaction) {
75
+ plugin.logerror('Transaction went away while delivering mail to qmail-queue')
76
+ try {
77
+ qmail_queue.stdout.end()
78
+ } catch (err) {
79
+ if (err.code !== 'ENOTCONN') {
80
+ // Ignore ENOTCONN and re throw anything else
81
+ throw err
82
+ }
83
+ }
84
+
85
+ connection.results.add(plugin, { err: 'dead sender' })
86
+ return
87
+ }
88
+ plugin.loginfo('Message Stream sent to qmail. Now sending envelope')
89
+ const buf = plugin.build_envelope(connection.transaction)
90
+ qmail_queue.stdout.on('error', () => {}) // stdout throws an error on close
91
+ qmail_queue.stdout.end(buf)
92
+ })
93
+ }
@@ -0,0 +1,133 @@
1
+ // quarantine
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+
6
+ exports.register = function () {
7
+ this.load_quarantine_ini()
8
+
9
+ this.register_hook('queue', 'quarantine')
10
+ this.register_hook('queue_outbound', 'quarantine')
11
+ }
12
+
13
+ exports.hook_init_master = function (next) {
14
+ this.init_quarantine_dir(() => {
15
+ this.clean_tmp_directory(next)
16
+ })
17
+ }
18
+
19
+ exports.load_quarantine_ini = function () {
20
+ this.cfg = this.config.get('quarantine.ini', () => {
21
+ this.load_quarantine_ini()
22
+ })
23
+ }
24
+
25
+ const zeroPad = (exports.zeroPad = (n, digits) => {
26
+ n = n.toString()
27
+ while (n.length < digits) {
28
+ n = `0${n}`
29
+ }
30
+ return n
31
+ })
32
+
33
+ exports.clean_tmp_directory = function (next) {
34
+ const tmp_dir = path.join(this.get_base_dir(), 'tmp')
35
+
36
+ if (fs.existsSync(tmp_dir)) {
37
+ const dirent = fs.readdirSync(tmp_dir)
38
+ this.loginfo(`Removing temporary files from: ${tmp_dir}`)
39
+ for (const element of dirent) {
40
+ fs.unlinkSync(path.join(tmp_dir, element))
41
+ }
42
+ }
43
+ next()
44
+ }
45
+
46
+ function wants_quarantine(connection) {
47
+ const { notes, transaction } = connection ?? {}
48
+
49
+ if (notes.quarantine) return notes.quarantine
50
+
51
+ if (transaction.notes.quarantine) return transaction.notes.quarantine
52
+
53
+ return transaction.notes.get('queue.wants') === 'quarantine'
54
+ }
55
+
56
+ exports.get_base_dir = function () {
57
+ if (this.cfg.main.quarantine_path) return this.cfg.main.quarantine_path
58
+ return '/var/spool/haraka/quarantine'
59
+ }
60
+
61
+ exports.init_quarantine_dir = function (done) {
62
+ const tmp_dir = path.join(this.get_base_dir(), 'tmp')
63
+ fs.promises
64
+ .mkdir(tmp_dir, { recursive: true })
65
+ .then(() => this.loginfo(`created ${tmp_dir}`))
66
+ .catch(() => this.logerror(`Unable to create ${tmp_dir}`))
67
+ .finally(done)
68
+ }
69
+
70
+ exports.quarantine = function (next, connection) {
71
+ const quarantine = wants_quarantine(connection)
72
+ this.logdebug(`quarantine: ${quarantine}`)
73
+ if (!quarantine) return next()
74
+
75
+ // Calculate date in YYYYMMDD format
76
+ const d = new Date()
77
+ const yyyymmdd = d.getFullYear() + zeroPad(d.getMonth() + 1, 2) + this.zeroPad(d.getDate(), 2)
78
+
79
+ let subdir = yyyymmdd
80
+ // Allow either boolean or a sub-directory to be specified
81
+
82
+ if (typeof quarantine !== 'boolean' && quarantine !== 1) {
83
+ subdir = path.join(quarantine, yyyymmdd)
84
+ }
85
+
86
+ const txn = connection?.transaction
87
+ if (!txn) return next()
88
+
89
+ const base_dir = this.get_base_dir()
90
+ const msg_dir = path.join(base_dir, subdir)
91
+ const tmp_path = path.join(base_dir, 'tmp', txn.uuid)
92
+ const msg_path = path.join(msg_dir, txn.uuid)
93
+
94
+ // Create all the directories recursively if they do not exist.
95
+ // Then write the file to a temporary directory first, once this is
96
+ // successful we hardlink the file to the final destination and then
97
+ // remove the temporary file to guarantee a complete file in the
98
+ // final destination.
99
+ fs.promises
100
+ .mkdir(msg_dir, { recursive: true })
101
+ .catch(() => {
102
+ connection.logerror(this, `Error creating directory: ${msg_dir}`)
103
+ next()
104
+ })
105
+ .then(() => {
106
+ const ws = fs.createWriteStream(tmp_path)
107
+
108
+ ws.on('error', (err) => {
109
+ connection.logerror(this, `Error writing quarantine file: ${err.message}`)
110
+ return next()
111
+ })
112
+ ws.on('close', () => {
113
+ fs.link(tmp_path, msg_path, (err) => {
114
+ if (err) {
115
+ connection.logerror(this, `Error writing quarantine file: ${err}`)
116
+ } else {
117
+ // Add a note to where we stored the message
118
+ txn.notes.quarantined = msg_path
119
+ txn.results.add(this, { pass: msg_path, emit: true })
120
+ // Now delete the temporary file
121
+ fs.unlink(tmp_path, () => {})
122
+ }
123
+ // Using notes.quarantine_action to decide what to do after the message is quarantined.
124
+ // Format can be either action = [ code, msg ] or action = code
125
+ const action = connection.notes.quarantine_action || txn.notes.quarantine_action
126
+ if (!action) return next()
127
+ if (Array.isArray(action)) return next(action[0], action[1])
128
+ return next(action)
129
+ })
130
+ })
131
+ txn.message_stream.pipe(ws, { line_endings: '\n' })
132
+ })
133
+ }
@@ -0,0 +1,45 @@
1
+ // Bridge to an SMTP server
2
+ // Overrides the MX and sets the same AUTH user and password
3
+
4
+ exports.register = function () {
5
+ this.load_flat_ini()
6
+ }
7
+
8
+ exports.load_flat_ini = function () {
9
+ this.cfg = this.config.get('smtp_bridge.ini', () => {
10
+ this.load_flat_ini()
11
+ })
12
+ }
13
+
14
+ exports.hook_data_post = (next, connection) => {
15
+ const txn = connection?.transaction
16
+ if (!txn) return next()
17
+
18
+ // Copy auth notes to transaction notes so they're available in hmail.todo.notes
19
+ txn.notes.auth_user = connection.notes.auth_user
20
+ txn.notes.auth_passwd = connection.notes.auth_passwd
21
+ return next()
22
+ }
23
+
24
+ exports.hook_get_mx = function (next, hmail) {
25
+ let priority = 10
26
+ if (this.cfg.main.priority) {
27
+ priority = this.cfg.main.priority
28
+ }
29
+ let authType = null
30
+ if (this.cfg.main.auth_type) {
31
+ authType = this.cfg.main.auth_type
32
+ }
33
+ let port = null
34
+ if (this.cfg.main.port) {
35
+ port = this.cfg.main.port
36
+ }
37
+ return next(OK, {
38
+ priority,
39
+ exchange: this.cfg.main.host,
40
+ port,
41
+ auth_type: authType,
42
+ auth_user: hmail.todo.notes.auth_user,
43
+ auth_pass: hmail.todo.notes.auth_passwd,
44
+ })
45
+ }
@@ -0,0 +1,371 @@
1
+ 'use strict'
2
+ // Forward to an SMTP server
3
+ // Opens the connection to the ongoing SMTP server at queue time
4
+ // and passes back any errors seen on the ongoing server to the
5
+ // originating server.
6
+
7
+ const url = require('node:url')
8
+
9
+ const smtp_client_mod = require('../../smtp_client')
10
+ const tls_socket = require('../../tls_socket')
11
+
12
+ exports.register = function () {
13
+ this.load_errs = []
14
+
15
+ this.load_smtp_forward_ini()
16
+
17
+ if (this.load_errs.length > 0) return
18
+
19
+ if (this.cfg.main.check_sender) {
20
+ this.register_hook('mail', 'check_sender')
21
+ }
22
+
23
+ if (this.cfg.main.check_recipient) {
24
+ this.register_hook('rcpt', 'check_recipient')
25
+ }
26
+
27
+ this.register_hook('queue', 'queue_forward')
28
+
29
+ if (this.cfg.main.enable_outbound) {
30
+ // deliver local message via smtp forward when relaying=true
31
+ this.register_hook('queue_outbound', 'queue_forward')
32
+ }
33
+
34
+ // may specify more specific [per-domain] outbound routes
35
+ this.register_hook('get_mx', 'get_mx')
36
+ }
37
+
38
+ exports.load_smtp_forward_ini = function () {
39
+ this.cfg = this.config.get(
40
+ 'smtp_forward.ini',
41
+ {
42
+ booleans: [
43
+ '-main.enable_tls',
44
+ '-main.enable_outbound',
45
+ 'main.one_message_per_rcpt',
46
+ '-main.check_sender',
47
+ '-main.check_recipient',
48
+ '*.enable_tls',
49
+ '*.enable_outbound',
50
+ '+tls.requestCert',
51
+ '+tls.honorCipherOrder',
52
+ '-tls.rejectUnauthorized',
53
+ ],
54
+ },
55
+ () => {
56
+ this.load_smtp_forward_ini()
57
+ },
58
+ )
59
+
60
+ // Build backend TLS options from tls.ini [main] + this plugin's [tls] section.
61
+ // Re-derived on every (re)load so SIGHUP picks up edits.
62
+ this.tls_options = tls_socket.load_plugin_tls_options(this.cfg.tls || {})
63
+ }
64
+
65
+ exports.get_config = function (conn) {
66
+ if (!conn.transaction) return this.cfg.main
67
+
68
+ let dom, address
69
+ if (this.cfg.main.domain_selector === 'mail_from') {
70
+ if (!conn.transaction.mail_from) return this.cfg.main
71
+ dom = conn.transaction.mail_from.host
72
+ address = conn.transaction.mail_from.address
73
+ } else {
74
+ if (!conn.transaction.rcpt_to[0]) return this.cfg.main
75
+ dom = conn.transaction.rcpt_to[0].host
76
+ }
77
+
78
+ if (address && this.cfg[address]) return this.cfg[address]
79
+ if (!dom) return this.cfg.main
80
+ if (!this.cfg[dom]) return this.cfg.main // no specific route
81
+
82
+ return this.cfg[dom]
83
+ }
84
+
85
+ exports.is_outbound_enabled = function (dom_cfg) {
86
+ if ('enable_outbound' in dom_cfg) return dom_cfg.enable_outbound // per-domain flag
87
+
88
+ return this.cfg.main.enable_outbound // follow the global configuration
89
+ }
90
+
91
+ exports.check_sender = function (next, connection, params) {
92
+ const txn = connection?.transaction
93
+ if (!txn) return
94
+
95
+ const email = params[0].address
96
+ if (!email) {
97
+ txn.results.add(this, { skip: 'mail_from.null', emit: true })
98
+ return next()
99
+ }
100
+
101
+ const domain = params[0].host.toLowerCase()
102
+ if (!this.cfg[domain]) return next()
103
+
104
+ // domain is defined in smtp_forward.ini
105
+ txn.notes.local_sender = true
106
+
107
+ if (!connection.relaying) {
108
+ txn.results.add(this, { fail: 'mail_from!spoof' })
109
+ return next(DENY, 'Spoofed MAIL FROM')
110
+ }
111
+
112
+ txn.results.add(this, { pass: 'mail_from' })
113
+ next()
114
+ }
115
+
116
+ exports.set_queue = function (connection, queue_wanted, domain) {
117
+ let dom_cfg = this.cfg[domain]
118
+ if (dom_cfg === undefined) dom_cfg = {}
119
+
120
+ if (!queue_wanted) queue_wanted = dom_cfg.queue || this.cfg.main.queue
121
+ if (!queue_wanted) return true
122
+
123
+ let dst_host = dom_cfg.host || this.cfg.main.host
124
+ if (dst_host) dst_host = `smtp://${dst_host}`
125
+
126
+ const notes = connection?.transaction?.notes
127
+ if (!notes) return false
128
+ if (!notes.get('queue.wants')) {
129
+ notes.set('queue.wants', queue_wanted)
130
+ if (dst_host) notes.set('queue.next_hop', dst_host)
131
+ return true
132
+ }
133
+
134
+ // multiple recipients with same destination
135
+ if (notes.get('queue.wants') === queue_wanted) {
136
+ if (!dst_host) return true
137
+
138
+ const next_hop = notes.get('queue.next_hop')
139
+ if (!next_hop) return true
140
+ if (next_hop === dst_host) return true
141
+ }
142
+
143
+ // multiple recipients with different forward host, soft deny
144
+ return false
145
+ }
146
+
147
+ exports.check_recipient = function (next, connection, params) {
148
+ const txn = connection?.transaction
149
+ if (!txn) return
150
+
151
+ const rcpt = params[0]
152
+ if (!rcpt.host) {
153
+ txn.results.add(this, { skip: 'rcpt!domain' })
154
+ return next()
155
+ }
156
+
157
+ if (connection.relaying && txn.notes.local_sender) {
158
+ this.set_queue(connection, 'outbound')
159
+ txn.results.add(this, { pass: 'relaying local_sender' })
160
+ return next(OK)
161
+ }
162
+
163
+ const domain = rcpt.host.toLowerCase()
164
+ if (this.cfg[domain] !== undefined) {
165
+ if (this.set_queue(connection, 'smtp_forward', domain)) {
166
+ txn.results.add(this, { pass: 'rcpt_to' })
167
+ return next(OK)
168
+ }
169
+ txn.results.add(this, { pass: 'rcpt_to.split' })
170
+ return next(DENYSOFT, 'Split transaction, retry soon')
171
+ }
172
+
173
+ // the MAIL FROM domain is not local and neither is the RCPT TO
174
+ // Another RCPT plugin may vouch for this recipient.
175
+ txn.results.add(this, { msg: 'rcpt!local' })
176
+ next()
177
+ }
178
+
179
+ exports.auth = function (cfg, connection, smtp_client) {
180
+ connection.loginfo(this, `Configuring authentication for SMTP server ${cfg.host}:${cfg.port}`)
181
+ smtp_client.on('capabilities', () => {
182
+ connection.loginfo(this, 'capabilities received')
183
+
184
+ if ('secured' in smtp_client) {
185
+ connection.loginfo(this, 'secured is pending')
186
+ if (smtp_client.secured === false) {
187
+ connection.loginfo(this, 'Waiting for STARTTLS to complete. AUTH postponed')
188
+ return
189
+ }
190
+ }
191
+
192
+ function base64(str) {
193
+ const buffer = Buffer.from(str, 'UTF-8')
194
+ return buffer.toString('base64')
195
+ }
196
+
197
+ if (cfg.auth_type === 'plain') {
198
+ connection.loginfo(this, `Authenticating with AUTH PLAIN ${cfg.auth_user}`)
199
+ smtp_client.send_command('AUTH', `PLAIN ${base64(`\0${cfg.auth_user}\0${cfg.auth_pass}`)}`)
200
+ return
201
+ }
202
+
203
+ if (cfg.auth_type === 'login') {
204
+ smtp_client.authenticating = true
205
+ smtp_client.authenticated = false
206
+
207
+ connection.loginfo(this, `Authenticating with AUTH LOGIN ${cfg.auth_user}`)
208
+ smtp_client.send_command('AUTH', 'LOGIN')
209
+ smtp_client.on('auth', () => {
210
+ // do nothing
211
+ })
212
+ smtp_client.on('auth_username', () => {
213
+ smtp_client.send_command(base64(cfg.auth_user))
214
+ })
215
+ smtp_client.on('auth_password', () => {
216
+ smtp_client.send_command(base64(cfg.auth_pass))
217
+ })
218
+ }
219
+ })
220
+ }
221
+
222
+ exports.forward_enabled = function (conn, dom_cfg) {
223
+ const q_wants = conn.transaction.notes.get('queue.wants')
224
+ if (q_wants && q_wants !== 'smtp_forward') {
225
+ conn.logdebug(this, `skipping, unwanted (${q_wants})`)
226
+ return false
227
+ }
228
+
229
+ if (conn.relaying && !this.is_outbound_enabled(dom_cfg)) {
230
+ conn.logdebug(this, 'skipping, outbound disabled')
231
+ return false
232
+ }
233
+
234
+ return true
235
+ }
236
+
237
+ exports.queue_forward = function (next, connection) {
238
+ const plugin = this
239
+ if (connection.remote.closed) return
240
+ const txn = connection?.transaction
241
+
242
+ const cfg = plugin.get_config(connection)
243
+ if (!plugin.forward_enabled(connection, cfg)) return next()
244
+
245
+ smtp_client_mod.get_client_plugin(plugin, connection, cfg, (err, smtp_client) => {
246
+ smtp_client.next = next
247
+
248
+ let rcpt = 0
249
+
250
+ if (cfg.auth_user) plugin.auth(cfg, connection, smtp_client)
251
+
252
+ connection.loginfo(
253
+ plugin,
254
+ `forwarding to ${cfg.forwarding_host_pool ? 'host_pool' : `${cfg.host}:${cfg.port}`}`,
255
+ )
256
+
257
+ function get_rs() {
258
+ return txn?.results ?? connection.results
259
+ }
260
+
261
+ function dead_sender() {
262
+ if (smtp_client.is_dead_sender(plugin, connection)) {
263
+ get_rs().add(plugin, { err: 'dead sender' })
264
+ return true
265
+ }
266
+ return false
267
+ }
268
+
269
+ function send_rcpt() {
270
+ if (dead_sender() || !txn) return
271
+ if (rcpt === txn.rcpt_to.length) {
272
+ smtp_client.send_command('DATA')
273
+ return
274
+ }
275
+ smtp_client.send_command('RCPT', `TO:${txn.rcpt_to[rcpt].format(!smtp_client.smtputf8)}`)
276
+ rcpt++
277
+ }
278
+
279
+ smtp_client.on('mail', send_rcpt)
280
+
281
+ if (cfg.one_message_per_rcpt) {
282
+ smtp_client.on('rcpt', () => {
283
+ smtp_client.send_command('DATA')
284
+ })
285
+ } else {
286
+ smtp_client.on('rcpt', send_rcpt)
287
+ }
288
+
289
+ smtp_client.on('data', () => {
290
+ if (dead_sender()) return
291
+ smtp_client.start_data(txn.message_stream)
292
+ })
293
+
294
+ smtp_client.on('dot', () => {
295
+ if (dead_sender() || !txn) return
296
+
297
+ get_rs().add(plugin, { pass: smtp_client.response })
298
+ if (rcpt < txn.rcpt_to.length) {
299
+ smtp_client.send_command('RSET')
300
+ return
301
+ }
302
+ smtp_client.call_next(OK, smtp_client.response)
303
+ smtp_client.release()
304
+ })
305
+
306
+ smtp_client.on('rset', () => {
307
+ if (dead_sender() || !txn) return
308
+ smtp_client.send_command('MAIL', `FROM:${txn.mail_from}`)
309
+ })
310
+
311
+ smtp_client.on('bad_code', (code, msg) => {
312
+ if (dead_sender() || !txn) return
313
+ smtp_client.call_next(code && code[0] === '5' ? DENY : DENYSOFT, msg)
314
+ smtp_client.release()
315
+ })
316
+ })
317
+ }
318
+
319
+ exports.get_mx_next_hop = (next_hop) => {
320
+ // queue.wants && queue.next_hop are mechanisms for fine-grained MX routing.
321
+ // Plugins can specify a queue to perform the delivery as well as a route. A
322
+ // plugin that uses this is qmail-deliverable, which can direct email delivery
323
+ // via smtp_forward, outbound (SMTP), and outbound (LMTP).
324
+ const dest = new url.URL(next_hop)
325
+ const mx = {
326
+ priority: 0,
327
+ port: dest.port || (dest.protocol === 'lmtp:' ? 24 : 25),
328
+ exchange: dest.hostname,
329
+ }
330
+ if (dest.protocol === 'lmtp:') mx.using_lmtp = true
331
+ if (dest.username) {
332
+ mx.auth_type = 'plain'
333
+ mx.auth_user = dest.username
334
+ mx.auth_pass = dest.password
335
+ }
336
+ return mx
337
+ }
338
+
339
+ exports.get_mx = function (next, hmail, domain) {
340
+ const qw = hmail.todo.notes.get('queue.wants')
341
+ if (qw && !['smtp_forward', 'outbound'].includes(qw)) return next()
342
+
343
+ if (qw === 'smtp_forward' && hmail.todo.notes.get('queue.next_hop')) {
344
+ return next(OK, this.get_mx_next_hop(hmail.todo.notes.get('queue.next_hop')))
345
+ }
346
+
347
+ const dom =
348
+ this.cfg.main.domain_selector === 'mail_from' ? hmail.todo.mail_from.host.toLowerCase() : domain.toLowerCase()
349
+ const cfg = this.cfg[dom]
350
+
351
+ if (cfg === undefined) {
352
+ this.logdebug(`using DNS MX for: ${domain}`)
353
+ return next()
354
+ }
355
+
356
+ const mx_opts = ['auth_type', 'auth_user', 'auth_pass', 'bind', 'bind_helo', 'using_lmtp']
357
+
358
+ const mx = {
359
+ priority: 0,
360
+ exchange: cfg.host || this.cfg.main.host,
361
+ port: cfg.port || this.cfg.main.port || 25,
362
+ }
363
+
364
+ // apply auth/mx options
365
+ for (const o of mx_opts) {
366
+ if (cfg[o] === undefined) continue
367
+ mx[o] = this.cfg[dom][o]
368
+ }
369
+
370
+ next(OK, mx)
371
+ }