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
package/plugins.js ADDED
@@ -0,0 +1,605 @@
1
+ 'use strict'
2
+ // load all defined plugins
3
+
4
+ // node built-ins
5
+ const fs = require('node:fs')
6
+ const path = require('node:path')
7
+ const vm = require('node:vm')
8
+
9
+ // npm modules
10
+ exports.config = require('haraka-config')
11
+ const constants = require('haraka-constants')
12
+
13
+ // local modules
14
+ const logger = require('./logger')
15
+
16
+ exports.registered_hooks = {}
17
+ exports.registered_plugins = {}
18
+ exports.plugin_list = []
19
+
20
+ let order = 0
21
+
22
+ class Plugin {
23
+ constructor(name) {
24
+ this.name = name
25
+ this.base = {}
26
+ this.timeout = get_timeout(name)
27
+ this._get_plugin_path()
28
+ this.config = this._get_config()
29
+ this.hooks = {}
30
+ }
31
+
32
+ haraka_require(name) {
33
+ return require(`./${name}`)
34
+ }
35
+
36
+ _get_plugin_path() {
37
+ /* From https://github.com/haraka/Haraka/pull/1278#issuecomment-168856528
38
+ In Development mode, or install via a plain "git clone":
39
+
40
+ Plain plugin in plugins/ folder
41
+ Plugin in a folder in plugins/<name>/ folder. Contains a package.json.
42
+ Plugin in node_modules. Contains a package.json file.
43
+
44
+ In "installed" mode (via haraka -i <path>):
45
+
46
+ Plain plugin in <path>/plugins/ folder
47
+ Plugin in a folder in <path>/plugins/<name>/folder. (same concept as above)
48
+ Plugin in <path>/node_modules. Contains a package.json file.
49
+ Core plugin in <core_haraka_dir>/plugins/ folder
50
+ Plugin in a folder in <core_haraka_dir>/plugins/<name>/ folder. (same concept as above)
51
+ Plugin in <core_haraka_dir>/node_modules.
52
+ */
53
+
54
+ this.hasPackageJson = false
55
+ const name = this.name.startsWith('haraka-plugin-') ? this.name.slice(14) : this.name
56
+ if (this.name !== name) this.name = name
57
+
58
+ let paths = []
59
+ if (process.env.HARAKA) {
60
+ // Installed mode - started via bin/haraka
61
+ paths = [...paths, ...plugin_search_paths(process.env.HARAKA, name)]
62
+
63
+ // permit local "folder" plugins (/$name/package.json) (see #1649)
64
+ paths.push(
65
+ path.resolve(process.env.HARAKA, 'plugins', name, 'package.json'),
66
+ path.resolve(process.env.HARAKA, 'node_modules', name, 'package.json'),
67
+ )
68
+ }
69
+
70
+ // development mode
71
+ paths = [...paths, ...plugin_search_paths(__dirname, name)]
72
+ for (const pp of paths) {
73
+ if (this.plugin_path) continue
74
+ try {
75
+ fs.statSync(pp)
76
+ this.plugin_path = pp
77
+ if (path.basename(pp) === 'package.json') {
78
+ this.hasPackageJson = true
79
+ }
80
+ } catch (ignore) {}
81
+ }
82
+ }
83
+
84
+ _get_config() {
85
+ if (this.hasPackageJson) {
86
+ // It's a package/folder plugin - look in plugin folder for defaults,
87
+ // haraka/config folder for overrides
88
+ return exports.config.module_config(path.dirname(this.plugin_path), process.env.HARAKA || __dirname)
89
+ }
90
+
91
+ if (process.env.HARAKA) {
92
+ // Plain .js file, installed mode - look in core folder for defaults,
93
+ // install dir for overrides
94
+ return exports.config.module_config(__dirname, process.env.HARAKA)
95
+ }
96
+
97
+ if (process.env.HARAKA_TEST_DIR) {
98
+ return exports.config.module_config(process.env.HARAKA_TEST_DIR)
99
+ }
100
+
101
+ // Plain .js file, git mode - just look in this folder
102
+ return exports.config.module_config(__dirname)
103
+ }
104
+
105
+ register_hook(hook_name, method_name, priority) {
106
+ priority = parseInt(priority)
107
+ if (!priority) priority = 0
108
+ if (priority > 100) priority = 100
109
+ if (priority < -100) priority = -100
110
+
111
+ if (!Array.isArray(exports.registered_hooks[hook_name])) {
112
+ exports.registered_hooks[hook_name] = []
113
+ }
114
+ exports.registered_hooks[hook_name].push({
115
+ plugin: this.name,
116
+ method: method_name,
117
+ priority,
118
+ timeout: this.timeout,
119
+ order: order++,
120
+ })
121
+ this.hooks[hook_name] = this.hooks[hook_name] || []
122
+ this.hooks[hook_name].push(method_name)
123
+
124
+ plugins.logdebug(`registered hook ${hook_name} to ${this.name}.${method_name} priority ${priority}`)
125
+ }
126
+
127
+ register() {} // noop
128
+
129
+ inherits(parent_name) {
130
+ const parent_plugin = plugins._load_and_compile_plugin(parent_name)
131
+ for (const method in parent_plugin) {
132
+ if (!this[method]) {
133
+ this[method] = parent_plugin[method]
134
+ }
135
+ }
136
+ if (parent_plugin.register) {
137
+ parent_plugin.register.call(this)
138
+ }
139
+ this.base[parent_name] = parent_plugin
140
+ }
141
+
142
+ _make_custom_require() {
143
+ return (module) => {
144
+ if (this.hasPackageJson) {
145
+ const mod = require(module)
146
+ constants.import(global)
147
+ global.server = plugins.server
148
+ return mod
149
+ }
150
+
151
+ if (module === './config') {
152
+ return this.config
153
+ }
154
+
155
+ if (!/^\./.test(module)) {
156
+ return require(module)
157
+ }
158
+
159
+ for (const ext of [`${module}.js`, module]) {
160
+ if (fs.existsSync(path.join(__dirname, ext))) {
161
+ return require(module)
162
+ }
163
+ }
164
+
165
+ return require(path.join(path.dirname(this.plugin_path), module))
166
+ }
167
+ }
168
+
169
+ _get_code(pi_path) {
170
+ if (this.hasPackageJson) {
171
+ let packageDir = path.dirname(pi_path)
172
+ if (/^win(32|64)/.test(process.platform)) {
173
+ // escape the c:\path\back\slashes else they disappear
174
+ packageDir = packageDir.replace(/\\/g, '\\\\')
175
+ }
176
+ return `var _p = require("${packageDir}"); for (var k in _p) { exports[k] = _p[k] }`
177
+ }
178
+
179
+ try {
180
+ return `"use strict";${fs.readFileSync(pi_path)}`
181
+ } catch (err) {
182
+ if (exports.config.get('smtp.ini').main.ignore_bad_plugins) {
183
+ plugins.logcrit(`Loading ${this.name} failed: ${err}`)
184
+ return
185
+ }
186
+ throw `Loading plugin ${this.name} failed: ${err}`
187
+ }
188
+ }
189
+
190
+ _compile() {
191
+ const pp = this.plugin_path
192
+ const code = this._get_code(pp)
193
+ if (!code) return
194
+
195
+ const sandbox = {
196
+ require: this._make_custom_require(),
197
+ __filename: pp,
198
+ __dirname: path.dirname(pp),
199
+ exports: this,
200
+ fetch,
201
+ clearTimeout,
202
+ clearInterval,
203
+ process,
204
+ setInterval,
205
+ setTimeout,
206
+ Buffer,
207
+ Math,
208
+ server: plugins.server,
209
+ setImmediate,
210
+ }
211
+ if (this.hasPackageJson) {
212
+ delete sandbox.__filename
213
+ }
214
+ constants.import(sandbox)
215
+ try {
216
+ vm.runInNewContext(code, sandbox, pp)
217
+ } catch (err) {
218
+ plugins.logcrit(`compiling '${this.name}' failed`)
219
+ if (exports.config.get('smtp.ini').main.ignore_bad_plugins) {
220
+ plugins.logcrit(`Loading '${this.name}' failed: ${err.message} - skipping`)
221
+ return
222
+ }
223
+ throw err // default is to re-throw and stop Haraka
224
+ }
225
+ }
226
+ }
227
+
228
+ exports.shutdown_plugins = () => {
229
+ for (const i in exports.registered_plugins) {
230
+ if (exports.registered_plugins[i].shutdown) {
231
+ exports.registered_plugins[i].shutdown()
232
+ }
233
+ }
234
+ }
235
+
236
+ process.on('message', (msg) => {
237
+ if (msg.event && msg.event == 'plugins.shutdown') {
238
+ plugins.loginfo('Shutting down')
239
+ exports.shutdown_plugins()
240
+ }
241
+ })
242
+
243
+ function plugin_search_paths(prefix, name) {
244
+ return [
245
+ // Haraka/plugins/*.js
246
+ path.resolve(prefix, 'plugins', `${name}.js`),
247
+
248
+ // Haraka/node_modules/haraka-plugin-*/package.json
249
+ path.resolve(prefix, 'node_modules', `haraka-plugin-${name}`, 'package.json'),
250
+
251
+ // global node_modules/haraka-plugin-*/package.json
252
+ path.resolve(prefix, '..', `haraka-plugin-${name}`, 'package.json'),
253
+ ]
254
+ }
255
+
256
+ function get_timeout(name) {
257
+ let timeout = parseFloat(exports.config.get(`${name}.timeout`))
258
+ if (isNaN(timeout)) {
259
+ plugins.logdebug(`no timeout in ${name}.timeout`)
260
+ timeout = parseFloat(exports.config.get('plugin_timeout'))
261
+ }
262
+ if (isNaN(timeout)) {
263
+ plugins.logdebug('no timeout in plugin_timeout')
264
+ timeout = 30
265
+ }
266
+
267
+ plugins.logdebug(`plugin ${name} timeout is: ${timeout}s`)
268
+ return timeout
269
+ }
270
+
271
+ logger.add_log_methods(Plugin)
272
+
273
+ const plugins = exports
274
+
275
+ logger.add_log_methods(plugins, 'plugins')
276
+
277
+ plugins.Plugin = Plugin
278
+
279
+ plugins.load_plugins = (override) => {
280
+ plugins.logdebug('Loading')
281
+ let plugin_list
282
+ if (override) {
283
+ if (!Array.isArray(override)) override = [override]
284
+ plugin_list = override
285
+ } else {
286
+ plugin_list = exports.config.get('plugins', 'list')
287
+ }
288
+
289
+ for (let plugin of plugin_list) {
290
+ if (plugin.startsWith('haraka-plugin-')) plugin = plugin.substring(14)
291
+ if (plugins.deprecated[plugin]) {
292
+ plugins.lognotice(
293
+ `${plugin} has been replaced by '${plugins.deprecated[plugin]}'. Please update config/plugins`,
294
+ )
295
+ plugins.load_plugin(plugins.deprecated[plugin])
296
+ } else {
297
+ plugins.load_plugin(plugin)
298
+ }
299
+ }
300
+
301
+ plugins.plugin_list = Object.keys(plugins.registered_plugins)
302
+
303
+ // Sort registered_hooks by priority
304
+ for (const hook of Object.keys(plugins.registered_hooks)) {
305
+ plugins.registered_hooks[hook].sort((a, b) => {
306
+ if (a.priority < b.priority) return -1
307
+ if (a.priority > b.priority) return 1
308
+ if (a.priority == b.priority) {
309
+ if (a.order > b.order) return 1
310
+ return -1
311
+ }
312
+ return 0
313
+ })
314
+ }
315
+
316
+ logger.dump_logs() // now logging plugins are loaded.
317
+ }
318
+
319
+ plugins.deprecated = {
320
+ 'auth/auth_ldap': 'auth-ldap',
321
+ backscatterer: 'dns-list',
322
+ 'connect.asn': 'asn',
323
+ 'connect.fcrdns': 'fcrdns',
324
+ 'connect.geoip': 'geoip',
325
+ 'connect.p0f': 'p0f',
326
+ 'connect.rdns_access': 'access',
327
+ 'data.nomsgid': 'headers',
328
+ 'data.noreceived': 'headers',
329
+ 'data.rfc5322_header_checks': 'headers',
330
+ 'data.headers': 'headers',
331
+ dkim_sign: 'dkim',
332
+ dkim_verify: 'dkim',
333
+ 'data.uribl': 'uribl',
334
+ dnsbl: 'dns-list',
335
+ dnswl: 'dns-list',
336
+ 'log.syslog': 'syslog',
337
+ 'mail_from.access': 'access',
338
+ 'mail_from.blocklist': 'access',
339
+ 'mail_from.nobounces': 'bounce',
340
+ max_unrecognized_commands: 'limit',
341
+ rate_limit: 'limit',
342
+ 'rcpt_to.access': 'access',
343
+ 'rcpt_to.blocklist': 'access',
344
+ 'rcpt_to.ldap': 'rcpt-ldap',
345
+ 'rcpt_to.max_count': 'limit',
346
+ 'rcpt_to.qmail_deliverable': 'qmail-deliverable',
347
+ 'rdns.regexp': 'access',
348
+ relay_acl: 'relay',
349
+ relay_all: 'relay',
350
+ relay_force_routing: 'relay',
351
+ }
352
+
353
+ plugins.load_plugin = (name) => {
354
+ plugins.loginfo(`loading ${name}`)
355
+
356
+ const plugin = plugins._load_and_compile_plugin(name)
357
+ if (plugin) {
358
+ plugins._register_plugin(plugin)
359
+ }
360
+
361
+ plugins.registered_plugins[name] = plugin
362
+ }
363
+
364
+ // Set in server.js; initialized to empty object
365
+ // to prevent it from blowing up any unit tests.
366
+ plugins.server = { notes: {} }
367
+
368
+ plugins._load_and_compile_plugin = (name) => {
369
+ const plugin = new Plugin(name)
370
+ if (!plugin.plugin_path) {
371
+ const err = `Loading plugin ${plugin.name} failed: No plugin with this name found`
372
+ if (exports.config.get('smtp.ini').main.ignore_bad_plugins) {
373
+ plugins.logcrit(err)
374
+ return
375
+ }
376
+ throw err
377
+ }
378
+ plugin._compile()
379
+ return plugin
380
+ }
381
+
382
+ plugins._register_plugin = (plugin) => {
383
+ plugin.register()
384
+
385
+ // register any hook_blah methods.
386
+ for (const method in plugin) {
387
+ const result = method.match(/^hook_(\w+)\b/)
388
+ if (result) {
389
+ plugin.register_hook(result[1], method)
390
+ }
391
+ }
392
+ }
393
+
394
+ plugins.run_hooks = (hook, object, params) => {
395
+ if (client_disconnected(object) && !is_required_hook(hook)) {
396
+ object.logdebug(`aborting ${hook} hook`)
397
+ return
398
+ }
399
+
400
+ if (hook !== 'log') object.logdebug(`running ${hook} hooks`)
401
+
402
+ if (is_required_hook(hook) && object.current_hook) {
403
+ object.current_hook[2]() // call cancel function
404
+ }
405
+
406
+ if (!is_required_hook(hook) && hook !== 'deny' && object.hooks_to_run && object.hooks_to_run.length) {
407
+ throw new Error('We are already running hooks! Fatal error!')
408
+ }
409
+
410
+ if (hook === 'deny') {
411
+ // Save the hooks_to_run list so that we can run any remaining
412
+ // plugins on the previous hook once this hook is complete.
413
+ object.saved_hooks_to_run = object.hooks_to_run
414
+ }
415
+ object.hooks_to_run = []
416
+
417
+ if (plugins.registered_hooks[hook]) {
418
+ for (const item of plugins.registered_hooks[hook]) {
419
+ const plugin = plugins.registered_plugins[item.plugin]
420
+ object.hooks_to_run.push([plugin, item.method])
421
+ }
422
+ }
423
+
424
+ plugins.run_next_hook(hook, object, params)
425
+ }
426
+
427
+ plugins.run_next_hook = (hook, object, params) => {
428
+ if (client_disconnected(object) && !is_required_hook(hook)) {
429
+ object.logdebug(`aborting ${hook} hook`)
430
+ return
431
+ }
432
+ let called_once = false
433
+ let timeout_id
434
+ let timed_out = false
435
+ let cancelled = false
436
+ let item
437
+
438
+ function cancel() {
439
+ if (timeout_id) clearTimeout(timeout_id)
440
+ cancelled = true
441
+ }
442
+ function callback(retval, msg) {
443
+ if (timeout_id) clearTimeout(timeout_id)
444
+ object.current_hook = null
445
+ if (cancelled) return // This hook has been cancelled
446
+
447
+ // Bail if client has disconnected
448
+ if (client_disconnected(object) && !is_required_hook(hook)) {
449
+ object.logdebug(`ignoring ${item[0].name} plugin callback`)
450
+ return
451
+ }
452
+ if (called_once && hook !== 'log') {
453
+ if (!timed_out) {
454
+ object.logerror(`${item[0].name} plugin ran callback ` + `multiple times - ignoring subsequent calls`)
455
+ // Write a stack trace to the log to aid debugging
456
+ object.logerror(new Error().stack)
457
+ }
458
+ return
459
+ }
460
+ called_once = true
461
+ if (!retval) retval = constants.cont
462
+
463
+ log_run_item(item, hook, retval, object, params, msg)
464
+
465
+ if (object.hooks_to_run.length !== 0) {
466
+ if (retval === constants.cont) {
467
+ return plugins.run_next_hook(hook, object, params)
468
+ }
469
+ if (hook === 'connect_init' || hook === 'disconnect') {
470
+ // these hooks ignore retval and always run for every plugin
471
+ return plugins.run_next_hook(hook, object, params)
472
+ }
473
+ }
474
+
475
+ const respond_method = `${hook}_respond`
476
+ if (item && is_deny_retval(retval) && hook.substring(0, 5) !== 'init_') {
477
+ object.deny_respond = get_denyfn(object, hook, params, retval, msg, respond_method)
478
+ plugins.run_hooks('deny', object, [retval, msg, item[0].name, item[1], params, hook])
479
+ } else {
480
+ object.hooks_to_run = []
481
+ object[respond_method](retval, msg, params)
482
+ }
483
+ }
484
+
485
+ if (!object.hooks_to_run.length) return callback()
486
+
487
+ // shift the next one off the stack and run it.
488
+ item = object.hooks_to_run.shift()
489
+ item.push(cancel)
490
+
491
+ if (hook !== 'log' && item[0].timeout) {
492
+ timeout_id = setTimeout(() => {
493
+ timed_out = true
494
+ object.logcrit(`Plugin ${item[0].name} timed out on hook ${hook} - make sure it calls the callback`)
495
+ callback(constants.denysoft, 'plugin timeout')
496
+ }, item[0].timeout * 1000)
497
+ }
498
+
499
+ if (hook !== 'log') {
500
+ object.logdebug(`running ${hook} hook in ${item[0].name} plugin`)
501
+ }
502
+
503
+ if (object.transaction?.notes.skip_plugins.includes(item[0].name)) {
504
+ object.logdebug(`skipping ${item[0].name}_${hook} by request in notes`)
505
+ return callback()
506
+ }
507
+
508
+ try {
509
+ object.current_hook = item
510
+ object.hook = hook
511
+ item[0][item[1]].call(item[0], callback, object, params)
512
+ } catch (err) {
513
+ if (hook !== 'log') {
514
+ object.logcrit(`Plugin ${item[0].name} failed: ${err.stack || err}`)
515
+ }
516
+ callback()
517
+ }
518
+ }
519
+
520
+ function client_disconnected(object) {
521
+ if (object.constructor.name === 'Connection' && object.state >= constants.connection.state.DISCONNECTING) {
522
+ object.logdebug('client has disconnected')
523
+ return true
524
+ }
525
+ return false
526
+ }
527
+
528
+ function is_required_hook(hook) {
529
+ // Hooks that must always run
530
+ switch (hook) {
531
+ case 'reset_transaction':
532
+ case 'disconnect':
533
+ case 'log':
534
+ return true
535
+ default:
536
+ return false
537
+ }
538
+ }
539
+
540
+ function log_run_item(item, hook, retval, object, params, msg) {
541
+ if (!item) return
542
+ if (hook === 'log') return
543
+
544
+ let log = 'logdebug'
545
+ const is_not_cont = retval !== constants.cont && logger.would_log(logger.LOGINFO)
546
+ if (is_not_cont) log = 'loginfo'
547
+ if (is_not_cont || logger.would_log(logger.LOGDEBUG)) {
548
+ object[log]({
549
+ hook,
550
+ plugin: item[0].name,
551
+ function: item[1],
552
+ params: params ? (typeof params === 'string' ? params : params[0]) : '',
553
+ retval: constants.translate(retval),
554
+ msg: sanitize(msg),
555
+ })
556
+ }
557
+ }
558
+
559
+ function sanitize(msg) {
560
+ if (!msg) return ''
561
+ if (typeof msg === 'string') return msg
562
+ if (typeof msg === 'object') {
563
+ if (msg.constructor.name === 'DSN') return msg.reply
564
+ const sanitized = { ...msg } // copy the message
565
+ for (const priv of ['password', 'auth_pass']) {
566
+ delete sanitized[priv]
567
+ }
568
+ return JSON.stringify(sanitized)
569
+ }
570
+ logger.logerror(`what is ${msg} (typeof ${typeof msg})?`)
571
+ }
572
+
573
+ function is_deny_retval(val) {
574
+ switch (val) {
575
+ case constants.deny:
576
+ case constants.denysoft:
577
+ case constants.denydisconnect:
578
+ case constants.denysoftdisconnect:
579
+ return true
580
+ }
581
+ return false
582
+ }
583
+
584
+ function get_denyfn(object, hook, params, retval, msg, respond_method) {
585
+ return (deny_retval, deny_msg) => {
586
+ switch (deny_retval) {
587
+ case constants.ok:
588
+ // Override rejection
589
+ object.loginfo(`deny(soft?) overridden by deny hook${deny_msg ? ': deny_msg' : ''}`)
590
+ // Restore hooks_to_run with saved copy so that
591
+ // any other plugins on this hook can also run.
592
+ if (object.saved_hooks_to_run.length > 0) {
593
+ object.hooks_to_run = object.saved_hooks_to_run
594
+ plugins.run_next_hook(hook, object, params)
595
+ } else {
596
+ object[respond_method](constants.cont, deny_msg, params)
597
+ }
598
+ break
599
+ default:
600
+ object.saved_hooks_to_run = []
601
+ object.hooks_to_run = []
602
+ object[respond_method](retval, msg, params)
603
+ }
604
+ }
605
+ }
package/run_tests ADDED
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+
3
+ # Files using node:test
4
+ NODE_TEST_FILES="test/*.js test/outbound/*.js test/plugins/*.js test/plugins/auth/*.js test/plugins/queue/*.js"
5
+
6
+ if [ -n "$1" ]; then
7
+ node --test "$1"
8
+ else
9
+ # default, run 'em all
10
+ node --test --test-concurrency=1 $NODE_TEST_FILES
11
+ fi