haraka 0.0.32 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/.githooks/pre-commit +41 -0
  3. package/.prettierignore +6 -0
  4. package/.qlty/.gitignore +7 -0
  5. package/.qlty/configs/.shellcheckrc +1 -0
  6. package/.qlty/qlty.toml +15 -0
  7. package/CHANGELOG.md +1872 -62
  8. package/CLAUDE.md +40 -0
  9. package/CONTRIBUTORS.md +34 -0
  10. package/Dockerfile +50 -0
  11. package/GEMINI.md +38 -0
  12. package/LICENSE +2 -1
  13. package/Plugins.md +227 -0
  14. package/README.md +100 -115
  15. package/SECURITY.md +178 -0
  16. package/TODO +22 -0
  17. package/address.js +53 -0
  18. package/bin/haraka +593 -0
  19. package/bin/haraka_grep +32 -0
  20. package/config/aliases +2 -0
  21. package/config/auth_flat_file.ini +7 -0
  22. package/config/auth_vpopmaild.ini +9 -0
  23. package/config/connection.ini +79 -0
  24. package/config/delay_deny.ini +7 -0
  25. package/config/dhparams.pem +8 -0
  26. package/config/host_list +3 -0
  27. package/config/host_list_regex +6 -0
  28. package/config/http.ini +11 -0
  29. package/config/lmtp.ini +7 -0
  30. package/config/log.ini +11 -0
  31. package/config/me +1 -0
  32. package/config/outbound.bounce_message +18 -0
  33. package/config/outbound.bounce_message_html +36 -0
  34. package/config/outbound.bounce_message_image +106 -0
  35. package/config/outbound.ini +24 -0
  36. package/config/plugins +67 -0
  37. package/config/smtp.ini +37 -0
  38. package/config/smtp_bridge.ini +4 -0
  39. package/config/smtp_forward.ini +31 -0
  40. package/config/smtp_proxy.ini +27 -0
  41. package/config/tarpit.timeout +1 -0
  42. package/config/tls.ini +83 -0
  43. package/config/tls_cert.pem +23 -0
  44. package/config/tls_key.pem +28 -0
  45. package/config/watch.ini +12 -0
  46. package/config/xclient.hosts +2 -0
  47. package/connection.js +1863 -0
  48. package/contrib/Haraka.cf +6 -0
  49. package/contrib/Haraka.pm +35 -0
  50. package/contrib/bad_smtp_server.pl +25 -0
  51. package/contrib/bsd-rc.d/haraka +61 -0
  52. package/contrib/debian-init.d/haraka +87 -0
  53. package/contrib/haraka.init +96 -0
  54. package/contrib/haraka.service +23 -0
  55. package/contrib/plugin2npm.sh +81 -0
  56. package/contrib/ubuntu-upstart/haraka.conf +27 -0
  57. package/coverage/coverage-final.json +2 -0
  58. package/coverage/coverage-summary.json +33 -0
  59. package/coverage/tmp/coverage-79131-1779241025146-0.json +1 -0
  60. package/coverage/tmp/coverage-79132-1779240999690-0.json +1 -0
  61. package/coverage/tmp/coverage-79172-1779241000095-0.json +1 -0
  62. package/coverage/tmp/coverage-79210-1779241000156-0.json +1 -0
  63. package/coverage/tmp/coverage-79211-1779241000209-0.json +1 -0
  64. package/coverage/tmp/coverage-79212-1779241000266-0.json +1 -0
  65. package/coverage/tmp/coverage-79213-1779241000441-0.json +1 -0
  66. package/coverage/tmp/coverage-79214-1779241000626-0.json +1 -0
  67. package/coverage/tmp/coverage-79215-1779241000795-0.json +1 -0
  68. package/coverage/tmp/coverage-79216-1779241000965-0.json +1 -0
  69. package/coverage/tmp/coverage-79218-1779241001013-0.json +1 -0
  70. package/coverage/tmp/coverage-79219-1779241001179-0.json +1 -0
  71. package/coverage/tmp/coverage-79220-1779241006249-0.json +1 -0
  72. package/coverage/tmp/coverage-79227-1779241011453-0.json +1 -0
  73. package/coverage/tmp/coverage-79229-1779241011537-0.json +1 -0
  74. package/coverage/tmp/coverage-79230-1779241011647-0.json +1 -0
  75. package/coverage/tmp/coverage-79231-1779241011765-0.json +1 -0
  76. package/coverage/tmp/coverage-79232-1779241011841-0.json +1 -0
  77. package/coverage/tmp/coverage-79233-1779241011909-0.json +1 -0
  78. package/coverage/tmp/coverage-79234-1779241011984-0.json +1 -0
  79. package/coverage/tmp/coverage-79235-1779241012055-0.json +1 -0
  80. package/coverage/tmp/coverage-79236-1779241012230-0.json +1 -0
  81. package/coverage/tmp/coverage-79237-1779241012300-0.json +1 -0
  82. package/coverage/tmp/coverage-79238-1779241012368-0.json +1 -0
  83. package/coverage/tmp/coverage-79239-1779241012438-0.json +1 -0
  84. package/coverage/tmp/coverage-79240-1779241012511-0.json +1 -0
  85. package/coverage/tmp/coverage-79241-1779241012582-0.json +1 -0
  86. package/coverage/tmp/coverage-79242-1779241012652-0.json +1 -0
  87. package/coverage/tmp/coverage-79243-1779241012814-0.json +1 -0
  88. package/coverage/tmp/coverage-79244-1779241012931-0.json +1 -0
  89. package/coverage/tmp/coverage-79245-1779241013007-0.json +1 -0
  90. package/coverage/tmp/coverage-79246-1779241013106-0.json +1 -0
  91. package/coverage/tmp/coverage-79247-1779241013178-0.json +1 -0
  92. package/coverage/tmp/coverage-79248-1779241013244-0.json +1 -0
  93. package/coverage/tmp/coverage-79249-1779241013409-0.json +1 -0
  94. package/coverage/tmp/coverage-79250-1779241013697-0.json +1 -0
  95. package/coverage/tmp/coverage-79251-1779241013847-0.json +1 -0
  96. package/coverage/tmp/coverage-79252-1779241014288-0.json +1 -0
  97. package/coverage/tmp/coverage-79253-1779241014378-0.json +1 -0
  98. package/coverage/tmp/coverage-79254-1779241014428-0.json +1 -0
  99. package/coverage/tmp/coverage-79255-1779241021774-0.json +1 -0
  100. package/coverage/tmp/coverage-80382-1779241021949-0.json +1 -0
  101. package/coverage/tmp/coverage-80383-1779241025019-0.json +1 -0
  102. package/coverage/tmp/coverage-80384-1779241025133-0.json +1 -0
  103. package/docs/Body.md +1 -0
  104. package/docs/Config.md +1 -0
  105. package/docs/Connection.md +153 -0
  106. package/docs/CoreConfig.md +96 -0
  107. package/docs/CustomReturnCodes.md +3 -0
  108. package/docs/HAProxy.md +62 -0
  109. package/docs/Header.md +1 -0
  110. package/docs/Logging.md +129 -0
  111. package/docs/Outbound.md +210 -0
  112. package/docs/Plugins.md +372 -0
  113. package/docs/Results.md +7 -0
  114. package/docs/Transaction.md +135 -0
  115. package/docs/Tutorial.md +183 -0
  116. package/docs/deprecated/access.md +3 -0
  117. package/docs/deprecated/backscatterer.md +9 -0
  118. package/docs/deprecated/connect.rdns_access.md +53 -0
  119. package/docs/deprecated/data.headers.md +3 -0
  120. package/docs/deprecated/data.nomsgid.md +7 -0
  121. package/docs/deprecated/data.noreceived.md +11 -0
  122. package/docs/deprecated/data.rfc5322_header_checks.md +11 -0
  123. package/docs/deprecated/dkim_sign.md +97 -0
  124. package/docs/deprecated/dkim_verify.md +28 -0
  125. package/docs/deprecated/dnsbl.md +80 -0
  126. package/docs/deprecated/dnswl.md +73 -0
  127. package/docs/deprecated/lookup_rdns.strict.md +67 -0
  128. package/docs/deprecated/mail_from.access.md +52 -0
  129. package/docs/deprecated/mail_from.blocklist.md +18 -0
  130. package/docs/deprecated/mail_from.nobounces.md +8 -0
  131. package/docs/deprecated/rcpt_to.access.md +53 -0
  132. package/docs/deprecated/rcpt_to.blocklist.md +18 -0
  133. package/docs/deprecated/rcpt_to.routes.md +3 -0
  134. package/docs/deprecated/rdns.regexp.md +30 -0
  135. package/docs/plugins/aliases.md +3 -0
  136. package/docs/plugins/auth/auth_bridge.md +34 -0
  137. package/docs/plugins/auth/auth_ldap.md +4 -0
  138. package/docs/plugins/auth/auth_proxy.md +36 -0
  139. package/docs/plugins/auth/auth_vpopmaild.md +33 -0
  140. package/docs/plugins/auth/flat_file.md +40 -0
  141. package/docs/plugins/block_me.md +18 -0
  142. package/docs/plugins/data.signatures.md +11 -0
  143. package/docs/plugins/delay_deny.md +23 -0
  144. package/docs/plugins/max_unrecognized_commands.md +6 -0
  145. package/docs/plugins/prevent_credential_leaks.md +22 -0
  146. package/docs/plugins/process_title.md +42 -0
  147. package/docs/plugins/queue/deliver.md +3 -0
  148. package/docs/plugins/queue/discard.md +32 -0
  149. package/docs/plugins/queue/lmtp.md +24 -0
  150. package/docs/plugins/queue/qmail-queue.md +16 -0
  151. package/docs/plugins/queue/quarantine.md +87 -0
  152. package/docs/plugins/queue/smtp_bridge.md +32 -0
  153. package/docs/plugins/queue/smtp_forward.md +127 -0
  154. package/docs/plugins/queue/smtp_proxy.md +68 -0
  155. package/docs/plugins/queue/test.md +7 -0
  156. package/docs/plugins/rcpt_to.in_host_list.md +34 -0
  157. package/docs/plugins/rcpt_to.max_count.md +3 -0
  158. package/docs/plugins/record_envelope_addresses.md +20 -0
  159. package/docs/plugins/relay.md +3 -0
  160. package/docs/plugins/reseed_rng.md +16 -0
  161. package/docs/plugins/status.md +41 -0
  162. package/docs/plugins/tarpit.md +50 -0
  163. package/docs/plugins/tls.md +235 -0
  164. package/docs/plugins/toobusy.md +27 -0
  165. package/docs/plugins/xclient.md +10 -0
  166. package/docs/tutorials/Migrating_from_v1_to_v2.md +96 -0
  167. package/docs/tutorials/SettingUpOutbound.md +62 -0
  168. package/eslint.config.mjs +2 -0
  169. package/haraka.js +74 -0
  170. package/haraka.sh +2 -0
  171. package/http/html/404.html +58 -0
  172. package/http/html/index.html +47 -0
  173. package/http/package.json +21 -0
  174. package/line_socket.js +24 -0
  175. package/logger.js +322 -0
  176. package/outbound/client_pool.js +59 -0
  177. package/outbound/config.js +134 -0
  178. package/outbound/hmail.js +1504 -0
  179. package/outbound/index.js +349 -0
  180. package/outbound/qfile.js +93 -0
  181. package/outbound/queue.js +399 -0
  182. package/outbound/tls.js +85 -0
  183. package/outbound/todo.js +17 -0
  184. package/package.json +91 -29
  185. package/plugins/.eslintrc.yaml +3 -0
  186. package/plugins/auth/auth_base.js +261 -0
  187. package/plugins/auth/auth_bridge.js +20 -0
  188. package/plugins/auth/auth_proxy.js +227 -0
  189. package/plugins/auth/auth_vpopmaild.js +162 -0
  190. package/plugins/auth/flat_file.js +44 -0
  191. package/plugins/block_me.js +88 -0
  192. package/plugins/data.signatures.js +30 -0
  193. package/plugins/delay_deny.js +153 -0
  194. package/plugins/prevent_credential_leaks.js +61 -0
  195. package/plugins/process_title.js +197 -0
  196. package/plugins/profile.js +11 -0
  197. package/plugins/queue/deliver.js +12 -0
  198. package/plugins/queue/discard.js +27 -0
  199. package/plugins/queue/lmtp.js +45 -0
  200. package/plugins/queue/qmail-queue.js +93 -0
  201. package/plugins/queue/quarantine.js +133 -0
  202. package/plugins/queue/smtp_bridge.js +45 -0
  203. package/plugins/queue/smtp_forward.js +371 -0
  204. package/plugins/queue/smtp_proxy.js +142 -0
  205. package/plugins/queue/test.js +15 -0
  206. package/plugins/rcpt_to.host_list_base.js +65 -0
  207. package/plugins/rcpt_to.in_host_list.js +56 -0
  208. package/plugins/record_envelope_addresses.js +17 -0
  209. package/plugins/reseed_rng.js +7 -0
  210. package/plugins/status.js +274 -0
  211. package/plugins/tarpit.js +45 -0
  212. package/plugins/tls.js +164 -0
  213. package/plugins/toobusy.js +47 -0
  214. package/plugins/xclient.js +124 -0
  215. package/plugins.js +604 -0
  216. package/queue/1772642154987_1775581346001_4_82235_TGwgfd_2_mattbook-m3.home.simerson.net +0 -0
  217. package/run_tests +11 -0
  218. package/server.js +827 -0
  219. package/smtp_client.js +504 -0
  220. package/test/.eslintrc.yaml +11 -0
  221. package/test/config/auth_flat_file.ini +5 -0
  222. package/test/config/block_me.recipient +1 -0
  223. package/test/config/block_me.senders +1 -0
  224. package/test/config/dhparams.pem +8 -0
  225. package/test/config/host_list +2 -0
  226. package/test/config/outbound_tls_cert.pem +1 -0
  227. package/test/config/outbound_tls_key.pem +1 -0
  228. package/test/config/plugins +7 -0
  229. package/test/config/smtp.ini +11 -0
  230. package/test/config/smtp_forward.ini +30 -0
  231. package/test/config/tls/example.com/_.example.com.key +28 -0
  232. package/test/config/tls/example.com/example.com.crt +25 -0
  233. package/test/config/tls/haraka.local.pem +51 -0
  234. package/test/config/tls.ini +45 -0
  235. package/test/config/tls_cert.pem +21 -0
  236. package/test/config/tls_key.pem +28 -0
  237. package/test/connection.js +817 -0
  238. package/test/fixtures/haproxy_allowed/config/connection.ini +3 -0
  239. package/test/fixtures/haproxy_disabled/config/connection.ini +3 -0
  240. package/test/fixtures/haproxy_untrusted/config/connection.ini +3 -0
  241. package/test/fixtures/line_socket.js +21 -0
  242. package/test/fixtures/todo_qfile.txt +0 -0
  243. package/test/fixtures/util_hmailitem.js +156 -0
  244. package/test/installation/config/test-plugin-flat +1 -0
  245. package/test/installation/config/test-plugin.ini +10 -0
  246. package/test/installation/config/tls.ini +1 -0
  247. package/test/installation/node_modules/load_first/index.js +5 -0
  248. package/test/installation/node_modules/load_first/package.json +11 -0
  249. package/test/installation/node_modules/test-plugin/config/test-plugin-flat +1 -0
  250. package/test/installation/node_modules/test-plugin/config/test-plugin.ini +9 -0
  251. package/test/installation/node_modules/test-plugin/package.json +5 -0
  252. package/test/installation/node_modules/test-plugin/test-plugin.js +5 -0
  253. package/test/installation/plugins/base_plugin.js +3 -0
  254. package/test/installation/plugins/folder_plugin/index.js +3 -0
  255. package/test/installation/plugins/folder_plugin/package.json +11 -0
  256. package/test/installation/plugins/inherits.js +7 -0
  257. package/test/installation/plugins/load_first.js +3 -0
  258. package/test/installation/plugins/plugin.js +1 -0
  259. package/test/installation/plugins/tls.js +3 -0
  260. package/test/logger.js +217 -0
  261. package/test/loud/config/dhparams.pem +0 -0
  262. package/test/loud/config/tls/goobered.pem +45 -0
  263. package/test/loud/config/tls.ini +43 -0
  264. package/test/mail_specimen/base64-root-part.txt +23 -0
  265. package/test/mail_specimen/varied-fold-lengths-preserve-data.txt +283 -0
  266. package/test/outbound/bounce_net_errors.js +133 -0
  267. package/test/outbound/bounce_rfc3464.js +226 -0
  268. package/test/outbound/hmail.js +210 -0
  269. package/test/outbound/index.js +385 -0
  270. package/test/outbound/qfile.js +124 -0
  271. package/test/outbound/queue.js +325 -0
  272. package/test/plugins/auth/auth_base.js +620 -0
  273. package/test/plugins/auth/auth_bridge.js +80 -0
  274. package/test/plugins/auth/auth_vpopmaild.js +81 -0
  275. package/test/plugins/auth/flat_file.js +123 -0
  276. package/test/plugins/block_me.js +141 -0
  277. package/test/plugins/data.signatures.js +111 -0
  278. package/test/plugins/delay_deny.js +262 -0
  279. package/test/plugins/prevent_credential_leaks.js +174 -0
  280. package/test/plugins/process_title.js +141 -0
  281. package/test/plugins/queue/deliver.js +98 -0
  282. package/test/plugins/queue/discard.js +78 -0
  283. package/test/plugins/queue/lmtp.js +137 -0
  284. package/test/plugins/queue/qmail-queue.js +98 -0
  285. package/test/plugins/queue/quarantine.js +80 -0
  286. package/test/plugins/queue/smtp_bridge.js +152 -0
  287. package/test/plugins/queue/smtp_forward.js +1023 -0
  288. package/test/plugins/queue/smtp_proxy.js +138 -0
  289. package/test/plugins/rcpt_to.host_list_base.js +102 -0
  290. package/test/plugins/rcpt_to.in_host_list.js +186 -0
  291. package/test/plugins/record_envelope_addresses.js +66 -0
  292. package/test/plugins/reseed_rng.js +34 -0
  293. package/test/plugins/status.js +207 -0
  294. package/test/plugins/tarpit.js +90 -0
  295. package/test/plugins/tls.js +86 -0
  296. package/test/plugins/toobusy.js +21 -0
  297. package/test/plugins/xclient.js +119 -0
  298. package/test/plugins.js +230 -0
  299. package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_fixed +0 -0
  300. package/test/queue/1507509981169_1507509981169_0_61403_e0Y0Ym_1_haraka +0 -0
  301. package/test/queue/1508269674999_1508269674999_0_34002_socVUF_1_haraka +0 -0
  302. package/test/queue/1508455115683_1508455115683_0_90253_9Q4o4V_1_haraka +0 -0
  303. package/test/queue/zero-length +0 -0
  304. package/test/server.js +1012 -0
  305. package/test/smtp_client.js +1303 -0
  306. package/test/tls_socket.js +321 -0
  307. package/test/transaction.js +554 -0
  308. package/tls_socket.js +771 -0
  309. package/transaction.js +267 -0
  310. package/lib/index.js +0 -371
@@ -0,0 +1,44 @@
1
+ // Auth against a flat file
2
+
3
+ exports.register = function () {
4
+ this.inherits('auth/auth_base')
5
+ this.load_flat_ini()
6
+
7
+ if (this.cfg.core.constrain_sender) {
8
+ this.register_hook('mail', 'constrain_sender')
9
+ }
10
+ }
11
+
12
+ exports.load_flat_ini = function () {
13
+ this.cfg = this.config.get(
14
+ 'auth_flat_file.ini',
15
+ {
16
+ booleans: ['+core.constrain_sender'],
17
+ },
18
+ () => {
19
+ this.load_flat_ini()
20
+ },
21
+ )
22
+
23
+ if (this.cfg.users === undefined) this.cfg.users = {}
24
+ }
25
+
26
+ exports.hook_capabilities = function (next, connection) {
27
+ if (!connection.remote.is_private && !connection.tls.enabled) {
28
+ connection.logdebug(this, 'Auth disabled for insecure public connection')
29
+ return next()
30
+ }
31
+
32
+ const methods = this.cfg.core?.methods ? this.cfg.core.methods.split(',') : null
33
+ if (methods && methods.length > 0) {
34
+ connection.capabilities.push(`AUTH ${methods.join(' ')}`)
35
+ connection.notes.allowed_auth_methods = methods
36
+ }
37
+ next()
38
+ }
39
+
40
+ exports.get_plain_passwd = function (user, connection, cb) {
41
+ if (this.cfg.users[user]) return cb(this.cfg.users[user].toString())
42
+
43
+ cb()
44
+ }
@@ -0,0 +1,88 @@
1
+ // Plugin which registers mail received to a certain address
2
+ // and extracts a From: address from the mail and puts that address
3
+ // in the mail_from.blocklist file. You need to be running the
4
+ // mail_from.blocklist plugin for this to work fully.
5
+
6
+ const fs = require('node:fs')
7
+ const path = require('node:path')
8
+ const utils = require('haraka-utils')
9
+
10
+ exports.hook_data = (next, connection) => {
11
+ // enable mail body parsing
12
+ connection.transaction.parse_body = true
13
+ next()
14
+ }
15
+
16
+ exports.hook_data_post = function (next, connection) {
17
+ if (!connection?.relaying || !connection?.transaction) return next()
18
+
19
+ const recip = (this.config.get('block_me.recipient') || '').toLowerCase()
20
+ const senders = this.config.get('block_me.senders', 'list')
21
+
22
+ // Make sure only 1 recipient
23
+ if (connection.transaction.rcpt_to.length != 1) {
24
+ return next()
25
+ }
26
+
27
+ // Check recipient is the right one
28
+ if (connection.transaction.rcpt_to[0].address.toLowerCase() != recip) {
29
+ return next()
30
+ }
31
+
32
+ // Check sender is in list
33
+ const sender = connection.transaction.mail_from.address
34
+ if (!utils.in_array(sender, senders)) {
35
+ return next(DENY, `You are not allowed to block mail, ${sender}`)
36
+ }
37
+
38
+ // Now extract the "From" from the body...
39
+ const to_block = extract_from_line(connection.transaction.body)
40
+ if (!to_block) {
41
+ connection.logerror(this, 'No sender found in email')
42
+ return next()
43
+ }
44
+
45
+ connection.loginfo(this, `Blocking new sender: ${to_block}`)
46
+
47
+ connection.transaction.notes.block_me = 1
48
+
49
+ // add to mail_from.blocklist, in the same config dir the plugin reads from
50
+ const blocklist = path.join(this.config.root_path, 'mail_from.blocklist')
51
+ fs.open(blocklist, 'a', (err, fd) => {
52
+ if (err) {
53
+ connection.logerror(this, `Unable to append to mail_from.blocklist: ${err}`)
54
+ return
55
+ }
56
+ fs.write(fd, `${to_block}\n`, null, 'UTF-8', () => {
57
+ fs.close(fd)
58
+ })
59
+ })
60
+
61
+ next()
62
+ }
63
+
64
+ exports.hook_queue = (next, connection) => {
65
+ if (connection.transaction.notes.block_me) {
66
+ // pretend we queued this mail
67
+ return next(OK)
68
+ }
69
+
70
+ next()
71
+ }
72
+
73
+ // Example: From: Site Tucano Gold <contato@tucanogold.com.br>
74
+ function extract_from_line(body) {
75
+ const matches = body.bodytext.match(/\bFrom:[^<\n]*<([^>\n]*)>/)
76
+ if (matches) {
77
+ return matches[1]
78
+ }
79
+
80
+ for (let i = 0, l = body.children.length; i < l; i++) {
81
+ const from = extract_from_line(body.children[i])
82
+ if (from) {
83
+ return from
84
+ }
85
+ }
86
+
87
+ return null
88
+ }
@@ -0,0 +1,30 @@
1
+ 'use strict'
2
+ // Simple string signatures
3
+
4
+ exports.hook_data = (next, connection) => {
5
+ // enable mail body parsing
6
+ if (connection?.transaction) connection.transaction.parse_body = true
7
+ next()
8
+ }
9
+
10
+ exports.hook_data_post = function (next, connection) {
11
+ if (!connection?.transaction) return next()
12
+
13
+ const sigs = this.config.get('data.signatures', 'list')
14
+
15
+ if (check_sigs(sigs, connection.transaction.body)) {
16
+ return next(DENY, 'Mail matches a known spam signature')
17
+ }
18
+ next()
19
+ }
20
+
21
+ function check_sigs(sigs, body) {
22
+ for (let i = 0, l = sigs.length; i < l; i++) {
23
+ if (body.bodytext.includes(sigs[i])) return 1
24
+ }
25
+
26
+ for (let i = 0, l = body.children.length; i < l; i++) {
27
+ if (check_sigs(sigs, body.children[i])) return 1
28
+ }
29
+ return 0
30
+ }
@@ -0,0 +1,153 @@
1
+ /*
2
+ ** delay_deny
3
+ **
4
+ ** This plugin delays all pre-DATA 'deny' results until the recipients are sent
5
+ ** and all post-DATA commands until all hook_data_post plugins have run.
6
+ ** This allows relays and authenticated users to bypass pre-DATA rejections.
7
+ */
8
+
9
+ exports.hook_deny = function (next, connection, params) {
10
+ /* params
11
+ ** [0] = plugin return value (DENY or DENYSOFT)
12
+ ** [1] = plugin return message
13
+ */
14
+
15
+ const pi_name = params[2]
16
+ const pi_function = params[3]
17
+ // var pi_params = params[4];
18
+ const pi_hook = params[5]
19
+
20
+ const { transaction } = connection
21
+
22
+ // Don't delay ourselves...
23
+ if (pi_name == 'delay_deny') return next()
24
+
25
+ // Load config
26
+ const cfg = this.config.get('delay_deny.ini')
27
+ let skip
28
+ let included
29
+ if (cfg.main.included_plugins) {
30
+ included = cfg.main.included_plugins.split(/[;, ]+/)
31
+ } else if (cfg.main.excluded_plugins) {
32
+ skip = cfg.main.excluded_plugins.split(/[;, ]+/)
33
+ }
34
+
35
+ // 'included' mode: only delay deny plugins in the included list
36
+ if (included?.length) {
37
+ if (
38
+ !included.includes(pi_name) &&
39
+ !included.includes(`${pi_name}:${pi_hook}`) &&
40
+ !included.includes(`${pi_name}:${pi_hook}:${pi_function}`)
41
+ ) {
42
+ return next()
43
+ }
44
+ } else if (skip?.length) {
45
+ // 'excluded' mode: delay deny everything except in skip list
46
+ // Skip by <plugin name>
47
+ if (skip.includes(pi_name)) {
48
+ connection.logdebug(this, `not delaying excluded plugin: ${pi_name}`)
49
+ return next()
50
+ }
51
+ // Skip by <plugin name>:<hook>
52
+ if (skip.includes(`${pi_name}:${pi_hook}`)) {
53
+ connection.logdebug(this, `not delaying excluded hook: ${pi_hook} in plugin: ${pi_name}`)
54
+ return next()
55
+ }
56
+ // Skip by <plugin name>:<hook>:<function name>
57
+ if (skip.includes(`${pi_name}:${pi_hook}:${pi_function}`)) {
58
+ connection.logdebug(
59
+ this,
60
+ `not delaying excluded function: ${pi_function} on hook: ${pi_hook} in plugin: ${pi_name}`,
61
+ )
62
+ return next()
63
+ }
64
+ }
65
+
66
+ switch (pi_hook) {
67
+ // Pre-DATA connection delays
68
+ case 'lookup_rdns':
69
+ case 'connect':
70
+ case 'ehlo':
71
+ case 'helo':
72
+ if (!connection.notes.delay_deny_pre) {
73
+ connection.notes.delay_deny_pre = []
74
+ }
75
+ connection.notes.delay_deny_pre.push(params)
76
+ if (!connection.notes.delay_deny_pre_fail) {
77
+ connection.notes.delay_deny_pre_fail = {}
78
+ }
79
+ connection.notes.delay_deny_pre_fail[pi_name] = 1
80
+ return next(OK)
81
+ // Pre-DATA transaction delays
82
+ case 'mail':
83
+ case 'rcpt':
84
+ case 'rcpt_ok':
85
+ if (!transaction.notes.delay_deny_pre) {
86
+ transaction.notes.delay_deny_pre = []
87
+ }
88
+ transaction.notes.delay_deny_pre.push(params)
89
+ if (!transaction.notes.delay_deny_pre_fail) {
90
+ transaction.notes.delay_deny_pre_fail = {}
91
+ }
92
+ transaction.notes.delay_deny_pre_fail[pi_name] = 1
93
+ return next(OK)
94
+ // Post-DATA delays
95
+ case 'data':
96
+ case 'data_post':
97
+ // fall through
98
+ default:
99
+ // No delays
100
+ next()
101
+ }
102
+ }
103
+
104
+ exports.hook_rcpt_ok = function (next, connection) {
105
+ const transaction = connection?.transaction
106
+ if (!transaction) return next()
107
+
108
+ // Bypass all pre-DATA deny for AUTH/RELAY
109
+ if (connection.relaying) {
110
+ connection.loginfo(this, 'bypassing all pre-DATA deny: AUTH/RELAY')
111
+ return next()
112
+ }
113
+
114
+ // Apply any delayed rejections
115
+ // Check connection level pre-DATA rejections first
116
+ if (connection.notes?.delay_deny_pre) {
117
+ for (const params of connection.notes.delay_deny_pre) {
118
+ return next(params[0], params[1])
119
+ }
120
+ }
121
+
122
+ // Then check transaction level pre-DATA
123
+ if (transaction.notes?.delay_deny_pre) {
124
+ for (let i = 0; i < transaction.notes.delay_deny_pre.length; i++) {
125
+ const params = transaction.notes.delay_deny_pre[i]
126
+
127
+ // Remove rejection from the array if it was on the rcpt hooks
128
+ if (params[5] === 'rcpt' || params[5] === 'rcpt_ok') {
129
+ transaction.notes.delay_deny_pre.splice(i, 1)
130
+ }
131
+
132
+ return next(params[0], params[1])
133
+ }
134
+ }
135
+ next()
136
+ }
137
+
138
+ exports.hook_data = (next, connection) => {
139
+ const transaction = connection?.transaction
140
+ if (!transaction) return next()
141
+
142
+ // Add a header showing all pre-DATA rejections
143
+ const fails = []
144
+ if (connection.notes?.delay_deny_pre_fail) {
145
+ fails.push.apply(Object.keys(connection.notes.delay_deny_pre_fail))
146
+ }
147
+ if (transaction.notes?.delay_deny_pre_fail) {
148
+ fails.push.apply(Object.keys(transaction.notes.delay_deny_pre_fail))
149
+ }
150
+ if (fails.length) transaction.add_header('X-Haraka-Fail-Pre', fails.join(' '))
151
+
152
+ next()
153
+ }
@@ -0,0 +1,61 @@
1
+ // Prevent a user from sending their AUTH credentials
2
+ // This is a simple, primitive form of anti-phishing.
3
+
4
+ function escapeRegExp(str) {
5
+ return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
6
+ }
7
+
8
+ exports.hook_data = (next, connection) => {
9
+ const { notes, transaction } = connection ?? {}
10
+
11
+ if (transaction && notes?.auth_user && notes.auth_passwd) {
12
+ transaction.parse_body = true
13
+ }
14
+ next()
15
+ }
16
+
17
+ exports.hook_data_post = (next, connection) => {
18
+ if (!(connection.notes.auth_user && connection.notes.auth_passwd)) {
19
+ return next()
20
+ }
21
+
22
+ let user = connection.notes.auth_user
23
+ let domain
24
+ const idx = user.indexOf('@')
25
+ if (idx > 0) {
26
+ // If the username is qualified (e.g. user@domain.com)
27
+ // then we make the @domain.com part optional in the regexp.
28
+ // (idx === -1 is "no @"; idx === 0 is a leading @ — neither is
29
+ // a qualified user@domain, so don't split.)
30
+ domain = user.substring(idx)
31
+ user = user.substring(0, idx)
32
+ }
33
+ const passwd = connection.notes.auth_passwd
34
+ const bound_regexp = '(?:\\b|\\B)'
35
+ const passwd_regexp = new RegExp(bound_regexp + escapeRegExp(passwd) + bound_regexp, 'm')
36
+ const user_regexp = new RegExp(
37
+ bound_regexp + escapeRegExp(user) + (domain ? `(?:${escapeRegExp(domain)})?` : '') + bound_regexp,
38
+ 'im',
39
+ )
40
+
41
+ if (look_for_credentials(user_regexp, passwd_regexp, connection?.transaction?.body)) {
42
+ return next(DENY, 'Credential leak detected: never give out your username/password to anyone!')
43
+ }
44
+
45
+ next()
46
+ }
47
+
48
+ function look_for_credentials(user_regexp, passwd_regexp, body) {
49
+ if (user_regexp.test(body.bodytext) && passwd_regexp.test(body.bodytext)) {
50
+ return true
51
+ }
52
+
53
+ // Check all child parts
54
+ for (let i = 0, l = body.children.length; i < l; i++) {
55
+ if (look_for_credentials(user_regexp, passwd_regexp, body.children[i])) {
56
+ return true
57
+ }
58
+ }
59
+
60
+ return false
61
+ }
@@ -0,0 +1,197 @@
1
+ // process_title
2
+
3
+ const outbound = require('../outbound')
4
+
5
+ function setupInterval(title, server) {
6
+ // Set up a timer to update title
7
+ return setInterval(() => {
8
+ // Connections per second
9
+ const av_cps = Math.round((server.notes.pt_connections / process.uptime()) * 100) / 100
10
+ const cps = server.notes.pt_connections - server.notes.pt_cps_diff
11
+ if (cps > server.notes.pt_cps_max) server.notes.pt_cps_max = cps
12
+ server.notes.pt_cps_diff = server.notes.pt_connections
13
+ // Recipients per second
14
+ const av_rps = Math.round((server.notes.pt_recipients / process.uptime()) * 100) / 100
15
+ const rps = server.notes.pt_recipients - server.notes.pt_rps_diff
16
+ if (rps > server.notes.pt_rps_max) server.notes.pt_rps_max = rps
17
+ server.notes.pt_rps_diff = server.notes.pt_recipients
18
+ // Recipients per message
19
+ const rpm = Math.round((server.notes.pt_recipients / server.notes.pt_messages) * 100) / 100 || 0
20
+ // Messages per second
21
+ const av_mps = Math.round((server.notes.pt_messages / process.uptime()) * 100) / 100
22
+ const mps = server.notes.pt_messages - server.notes.pt_mps_diff
23
+ if (mps > server.notes.pt_mps_max) server.notes.pt_mps_max = mps
24
+ server.notes.pt_mps_diff = server.notes.pt_messages
25
+ // Messages per connection
26
+ const mpc = Math.round((server.notes.pt_messages / server.notes.pt_connections) * 100) / 100 || 0
27
+
28
+ const out = server.notes.pt_out_stats || outbound.get_stats()
29
+ if (/\(worker\)/.test(title)) {
30
+ process.send({ event: 'process_title.outbound_stats', data: out })
31
+ }
32
+ // Update title
33
+ let new_title = `${title} cn=${server.notes.pt_connections} cc=${server.notes.pt_concurrent} cps=${cps}/${av_cps}/${server.notes.pt_cps_max} rcpts=${server.notes.pt_recipients}/${rpm} rps=${rps}/${av_rps}/${server.notes.pt_rps_max} msgs=${server.notes.pt_messages}/${mpc} mps=${mps}/${av_mps}/${server.notes.pt_mps_max} out=${out} `
34
+ if (/\(master\)/.test(title)) {
35
+ new_title += `respawn=${server.notes.pt_child_exits} `
36
+ }
37
+ process.title = new_title
38
+ }, 1000)
39
+ }
40
+
41
+ exports.hook_init_master = function (next, server) {
42
+ server.notes.pt_connections = 0
43
+ server.notes.pt_concurrent = 0
44
+ server.notes.pt_cps_diff = 0
45
+ server.notes.pt_cps_max = 0
46
+ server.notes.pt_recipients = 0
47
+ server.notes.pt_rps_diff = 0
48
+ server.notes.pt_rps_max = 0
49
+ server.notes.pt_messages = 0
50
+ server.notes.pt_mps_diff = 0
51
+ server.notes.pt_mps_max = 0
52
+ server.notes.pt_child_exits = 0
53
+ let title = 'Haraka'
54
+ if (server.cluster) {
55
+ title = 'Haraka (master)'
56
+ process.title = title
57
+ server.notes.pt_concurrent_cluster = {}
58
+ server.notes.pt_new_out_stats = [0, 0, 0, 0]
59
+ const { cluster } = server
60
+ const recvMsg = (msg) => {
61
+ let count
62
+ switch (msg.event) {
63
+ case 'process_title.connect':
64
+ server.notes.pt_connections++
65
+ server.notes.pt_concurrent_cluster[msg.wid]++
66
+ count = 0
67
+ for (const id of Object.keys(server.notes.pt_concurrent_cluster)) {
68
+ count += server.notes.pt_concurrent_cluster[id]
69
+ }
70
+ server.notes.pt_concurrent = count
71
+ break
72
+ case 'process_title.disconnect':
73
+ server.notes.pt_concurrent_cluster[msg.wid]--
74
+ count = 0
75
+ for (const id of Object.keys(server.notes.pt_concurrent_cluster)) {
76
+ count += server.notes.pt_concurrent_cluster[id]
77
+ }
78
+ server.notes.pt_concurrent = count
79
+ break
80
+ case 'process_title.recipient':
81
+ server.notes.pt_recipients++
82
+ break
83
+ case 'process_title.message':
84
+ server.notes.pt_messages++
85
+ break
86
+ case 'process_title.outbound_stats': {
87
+ const out_stats = msg.data.split('/')
88
+ for (let i = 0; i < out_stats.length; i++) {
89
+ server.notes.pt_new_out_stats[i] += parseInt(out_stats[i], 10)
90
+ }
91
+ server.notes.pt_new_out_stats[3]++
92
+ // Check if we got all results back yet
93
+ if (server.notes.pt_new_out_stats[3] === Object.keys(cluster.workers).length) {
94
+ server.notes.pt_out_stats = server.notes.pt_new_out_stats.slice(0, 3).join('/')
95
+ server.notes.pt_new_out_stats = [0, 0, 0, 0]
96
+ }
97
+ }
98
+ // fall through
99
+ default:
100
+ // Unknown message
101
+ }
102
+ }
103
+ // Register any new workers
104
+ cluster.on('fork', (worker) => {
105
+ server.notes.pt_concurrent_cluster[worker.id] = 0
106
+ cluster.workers[worker.id].on('message', recvMsg)
107
+ })
108
+ cluster.on('exit', (worker) => {
109
+ delete server.notes.pt_concurrent_cluster[worker.id]
110
+ // Update concurrency
111
+ let count = 0
112
+ for (const id of Object.keys(server.notes.pt_concurrent_cluster)) {
113
+ count += server.notes.pt_concurrent_cluster[id]
114
+ }
115
+ server.notes.pt_concurrent = count
116
+ server.notes.pt_child_exits++
117
+ })
118
+ }
119
+ this._interval = setupInterval(title, server)
120
+ next()
121
+ }
122
+
123
+ exports.hook_init_child = function (next, server) {
124
+ server.notes.pt_connections = 0
125
+ server.notes.pt_concurrent = 0
126
+ server.notes.pt_cps_diff = 0
127
+ server.notes.pt_cps_max = 0
128
+ server.notes.pt_recipients = 0
129
+ server.notes.pt_rps_diff = 0
130
+ server.notes.pt_rps_max = 0
131
+ server.notes.pt_messages = 0
132
+ server.notes.pt_mps_diff = 0
133
+ server.notes.pt_mps_max = 0
134
+ process.title = 'Haraka (worker)'
135
+ this._interval = setupInterval(process.title, server)
136
+ next()
137
+ }
138
+
139
+ exports.shutdown = function () {
140
+ this.logdebug(`Shutting down interval: ${this._interval}`)
141
+ clearInterval(this._interval)
142
+ }
143
+
144
+ exports.hook_connect_init = (next, connection) => {
145
+ const { server } = connection
146
+ connection.notes.pt_connect_run = true
147
+ if (server.cluster) {
148
+ const { worker } = server.cluster
149
+ worker.send({ event: 'process_title.connect', wid: worker.id })
150
+ }
151
+ server.notes.pt_connections++
152
+ server.notes.pt_concurrent++
153
+ next()
154
+ }
155
+
156
+ exports.hook_disconnect = (next, connection) => {
157
+ const { server } = connection
158
+ // Check that the hook above ran
159
+ // It might not if the disconnection is immediate
160
+ // echo "QUIT" | nc localhost 25
161
+ // will exhibit this behaviour.
162
+ let worker
163
+ if (!connection.notes.pt_connect_run) {
164
+ if (server.cluster) {
165
+ worker = server.cluster.worker
166
+ worker.send({ event: 'process_title.connect', wid: worker.id })
167
+ }
168
+ server.notes.pt_connections++
169
+ server.notes.pt_concurrent++
170
+ }
171
+ if (server.cluster) {
172
+ worker = server.cluster.worker
173
+ worker.send({ event: 'process_title.disconnect', wid: worker.id })
174
+ }
175
+ server.notes.pt_concurrent--
176
+ next()
177
+ }
178
+
179
+ exports.hook_rcpt = (next, connection) => {
180
+ const { server } = connection
181
+ if (server.cluster) {
182
+ const { worker } = server.cluster
183
+ worker.send({ event: 'process_title.recipient' })
184
+ }
185
+ server.notes.pt_recipients++
186
+ next()
187
+ }
188
+
189
+ exports.hook_data = (next, connection) => {
190
+ const { server } = connection
191
+ if (server.cluster) {
192
+ const { worker } = server.cluster
193
+ worker.send({ event: 'process_title.message' })
194
+ }
195
+ server.notes.pt_messages++
196
+ next()
197
+ }
@@ -0,0 +1,11 @@
1
+ const prof = require('v8-profiler')
2
+
3
+ exports.hook_connect_init = (next, conn) => {
4
+ prof.startProfiling(`Connection from: ${conn.remote.ip}`)
5
+ next()
6
+ }
7
+
8
+ exports.hook_disconnect = (next, conn) => {
9
+ prof.stopProfiling(`Connection from: ${conn.remote.ip}`)
10
+ next()
11
+ }
@@ -0,0 +1,12 @@
1
+ // This plugin is entirely redundant. The core will queue outbound mails
2
+ // automatically just like this. It is kept here for backwards compatibility
3
+ // purposes only.
4
+
5
+ const outbound = require('./outbound')
6
+
7
+ exports.hook_queue_outbound = (next, connection) => {
8
+ // if not relaying, don't deliver outbound
9
+ if (!connection?.relaying) return next()
10
+
11
+ outbound.send_trans_email(connection?.transaction, next)
12
+ }
@@ -0,0 +1,27 @@
1
+ // discard
2
+
3
+ exports.register = function () {
4
+ this.register_hook('queue', 'discard')
5
+ this.register_hook('queue_outbound', 'discard')
6
+ }
7
+
8
+ exports.discard = (next, connection) => {
9
+ const txn = connection.transaction
10
+
11
+ const q_wants = txn.notes.get('queue.wants')
12
+ if (q_wants && q_wants !== 'discard') return next()
13
+
14
+ function discard() {
15
+ connection.loginfo('discarding message')
16
+ // Pretend we delivered the message
17
+ return next(OK)
18
+ }
19
+
20
+ if (connection.notes.discard) return discard()
21
+ if (txn.notes.discard) return discard()
22
+ if (q_wants === 'discard') return discard()
23
+ if (process.env.YES_REALLY_DO_DISCARD) return discard()
24
+
25
+ // Allow other queue plugins to deliver
26
+ next()
27
+ }
@@ -0,0 +1,45 @@
1
+ //queue/lmtp
2
+
3
+ 'use strict'
4
+
5
+ let outbound
6
+
7
+ exports.register = function () {
8
+ this.load_lmtp_ini()
9
+ outbound = this.haraka_require('outbound')
10
+ }
11
+
12
+ exports.load_lmtp_ini = function () {
13
+ this.cfg = this.config.get('lmtp.ini', () => {
14
+ this.load_lmtp_ini()
15
+ })
16
+ }
17
+
18
+ exports.hook_get_mx = function (next, hmail, domain) {
19
+ if (!hmail.todo.notes.using_lmtp) return next()
20
+
21
+ const section = this.cfg[domain] || this.cfg.main
22
+
23
+ const mx = {
24
+ using_lmtp: true,
25
+ priority: 0,
26
+ exchange: section.host ?? '127.0.0.1',
27
+ port: section.port ?? 24,
28
+ }
29
+
30
+ if (section.path) mx.path = section.path
31
+
32
+ next(OK, mx)
33
+ }
34
+
35
+ exports.hook_queue = (next, connection) => {
36
+ const txn = connection?.transaction
37
+ if (!txn) return next()
38
+
39
+ const q_wants = txn.notes.get('queue.wants')
40
+
41
+ if (q_wants && q_wants !== 'lmtp') return next()
42
+
43
+ txn.notes.using_lmtp = true
44
+ outbound.send_trans_email(txn, next)
45
+ }