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/connection.js ADDED
@@ -0,0 +1,1865 @@
1
+ 'use strict'
2
+ // a single connection
3
+
4
+ const dns = require('node:dns')
5
+ const os = require('node:os')
6
+
7
+ // npm libs
8
+ const ipaddr = require('ipaddr.js')
9
+ const config = require('haraka-config')
10
+ const constants = require('haraka-constants')
11
+ const net_utils = require('haraka-net-utils')
12
+ const Notes = require('haraka-notes')
13
+ const utils = require('haraka-utils')
14
+ const { Address } = require('@haraka/email-address')
15
+ const ResultStore = require('haraka-results')
16
+
17
+ // Haraka libs
18
+ const logger = require('./logger')
19
+ const trans = require('./transaction')
20
+ const plugins = require('./plugins')
21
+ const rfc1869 = utils.rfc1869
22
+ const outbound = require('./outbound')
23
+
24
+ const states = constants.connection.state
25
+
26
+ const cfg = config.get('connection.ini', {
27
+ booleans: [
28
+ '-main.strict_rfc1869',
29
+ '-main.postel',
30
+ '+main.smtputf8',
31
+ '+headers.add_received',
32
+ '+headers.show_version',
33
+ '+headers.clean_auth_results',
34
+ ],
35
+ })
36
+
37
+ class Connection {
38
+ constructor(client, server) {
39
+ this.client = client
40
+ this.server = server
41
+
42
+ this.local = {
43
+ ip: null,
44
+ port: null,
45
+ host: net_utils.get_primary_host_name(),
46
+ info: 'Haraka',
47
+ }
48
+ this.remote = {
49
+ ip: null,
50
+ port: null,
51
+ host: null,
52
+ info: null,
53
+ closed: false,
54
+ is_private: false,
55
+ is_local: false,
56
+ }
57
+ this.hello = {
58
+ host: null,
59
+ verb: null,
60
+ }
61
+ this.tls = {
62
+ enabled: false,
63
+ advertised: false,
64
+ verified: false,
65
+ cipher: {},
66
+ }
67
+ this.proxy = {
68
+ allowed: false,
69
+ ip: null,
70
+ type: null,
71
+ timer: null,
72
+ }
73
+ this.set('tls', 'enabled', !!server.has_tls)
74
+
75
+ this.current_data = null
76
+ this.current_line = null
77
+ this.state = states.PAUSE
78
+ this.encoding = 'utf8'
79
+ this.prev_state = null
80
+ this.loop_code = null
81
+ this.loop_msg = null
82
+ this.uuid = utils.uuid()
83
+ this.notes = new Notes()
84
+ this.transaction = null
85
+ this.tran_count = 0
86
+ this.capabilities = null
87
+ this.early_talker = false
88
+ this.pipelining = false
89
+ this._relaying = false
90
+ this.esmtp = false
91
+ this.last_response = null
92
+ this.hooks_to_run = []
93
+ this.start_time = Date.now()
94
+ this.last_reject = ''
95
+ this.totalbytes = 0
96
+ this.rcpt_count = {
97
+ accept: 0,
98
+ tempfail: 0,
99
+ reject: 0,
100
+ }
101
+ this.msg_count = {
102
+ accept: 0,
103
+ tempfail: 0,
104
+ reject: 0,
105
+ }
106
+ this.results = new ResultStore(this)
107
+ this.errors = 0
108
+ this.last_rcpt_msg = null
109
+ this.hook = null
110
+ if (cfg.headers.show_version) {
111
+ this.local.info += `/${utils.getVersion(__dirname)}`
112
+ }
113
+ Connection.setupClient(this)
114
+ }
115
+ static setupClient(self) {
116
+ const ip = self.client.remoteAddress
117
+ if (!ip) {
118
+ self.logdebug('setupClient got no IP address for this connection!')
119
+ self.client.destroy()
120
+ return
121
+ }
122
+
123
+ const local_addr = self.server.address()
124
+ self.set('local', 'ip', ipaddr.process(self.client.localAddress || local_addr.address).toString())
125
+ self.set('local', 'port', self.client.localPort || local_addr.port)
126
+ self.results.add({ name: 'local' }, self.local)
127
+
128
+ self.set('remote', 'ip', ipaddr.process(ip).toString())
129
+ self.set('remote', 'port', self.client.remotePort)
130
+ self.results.add({ name: 'remote' }, self.remote)
131
+
132
+ self.lognotice('connect', {
133
+ ip: self.remote.ip,
134
+ port: self.remote.port,
135
+ local_ip: self.local.ip,
136
+ local_port: self.local.port,
137
+ })
138
+
139
+ if (!self.client.on) return
140
+
141
+ const log_data = { ip: self.remote.ip }
142
+ if (self.remote.host) log_data.host = self.remote.host
143
+
144
+ self.client.on('end', () => {
145
+ if (self.state >= states.DISCONNECTING) return
146
+ self.remote.closed = true
147
+ self.loginfo('client half closed connection', log_data)
148
+ self.fail()
149
+ })
150
+
151
+ self.client.on('close', () => {
152
+ if (self.state >= states.DISCONNECTING) return
153
+ self.remote.closed = true
154
+ self.loginfo('client dropped connection', log_data)
155
+ self.fail()
156
+ })
157
+
158
+ self.client.on('error', (err) => {
159
+ if (self.state >= states.DISCONNECTING) return
160
+ self.loginfo(`client connection error: ${err}`, log_data)
161
+ self.fail()
162
+ })
163
+
164
+ self.client.on('timeout', () => {
165
+ // FIN has sent, when timeout just destroy socket
166
+ if (self.state >= states.DISCONNECTED) {
167
+ self.client.destroy()
168
+ self.loginfo(`timeout, destroy socket (state:${self.state})`)
169
+ return
170
+ }
171
+ if (self.state >= states.DISCONNECTING) return
172
+ self.respond(421, 'timeout', () => {
173
+ self.fail('client connection timed out', log_data)
174
+ })
175
+ })
176
+
177
+ self.client.on('data', (data) => {
178
+ self.process_data(data)
179
+ })
180
+
181
+ // SMTPS pre-parser state: proxy means the PROXY line was already consumed;
182
+ // peer_allowed means a trusted PROXY peer sent direct TLS instead.
183
+ const smtps = self.client.haraka_smtps
184
+ if (smtps?.proxy) {
185
+ self.proxy.allowed = true
186
+ return
187
+ }
188
+
189
+ if (smtps?.peer_allowed) {
190
+ plugins.run_hooks('connect_init', self)
191
+ return
192
+ }
193
+
194
+ if (net_utils.is_haproxy_allowed(self.remote.ip)) {
195
+ self.proxy.allowed = true
196
+ // Wait for PROXY command
197
+ self.proxy.timer = setTimeout(() => {
198
+ self.respond(421, 'PROXY timeout', () => {
199
+ self.disconnect()
200
+ })
201
+ }, 30 * 1000)
202
+ } else {
203
+ plugins.run_hooks('connect_init', self)
204
+ }
205
+ }
206
+ setTLS(obj) {
207
+ this.set('hello', 'host', undefined)
208
+ this.set('tls', 'enabled', true)
209
+ for (const t of ['cipher', 'verified', 'verifyError', 'peerCertificate']) {
210
+ if (obj[t] === undefined) continue
211
+ this.set('tls', t, obj[t])
212
+ }
213
+ // prior to 2017-07, authorized and verified were both used. Verified
214
+ // seems to be the more common and has the property updated in the
215
+ // tls object. However, authorized has been up-to-date in the notes. Store
216
+ // in both, for backwards compatibility.
217
+ this.notes.tls = {
218
+ authorized: obj.verified, // legacy name
219
+ authorizationError: obj.verifyError,
220
+ cipher: obj.cipher,
221
+ peerCertificate: obj.peerCertificate,
222
+ }
223
+ }
224
+ set(prop_str, val) {
225
+ if (arguments.length === 3) {
226
+ prop_str = `${arguments[0]}.${arguments[1]}`
227
+ val = arguments[2]
228
+ }
229
+
230
+ const path_parts = prop_str.split('.')
231
+ let loc = this
232
+ for (let i = 0; i < path_parts.length; i++) {
233
+ const part = path_parts[i]
234
+ if (part === '__proto__' || part === 'constructor') continue
235
+
236
+ // while another part remains
237
+ if (i < path_parts.length - 1) {
238
+ if (loc[part] === undefined) loc[part] = {} // initialize
239
+ loc = loc[part] // descend
240
+ continue
241
+ }
242
+
243
+ // last part, so assign the value
244
+ loc[part] = val
245
+ }
246
+
247
+ // Set is_private, is_local automatically when remote.ip is set
248
+ if (prop_str === 'remote.ip') {
249
+ this.set('remote.is_local', net_utils.is_local_ip(this.remote.ip))
250
+ if (this.remote.is_local) {
251
+ this.set('remote.is_private', true)
252
+ } else {
253
+ this.set('remote.is_private', net_utils.is_private_ip(this.remote.ip))
254
+ }
255
+ }
256
+ }
257
+ get(prop_str) {
258
+ return prop_str.split('.').reduce((prev, curr) => {
259
+ return prev ? prev[curr] : undefined
260
+ }, this)
261
+ }
262
+ set relaying(val) {
263
+ if (this.transaction) {
264
+ this.transaction._relaying = val
265
+ } else {
266
+ this._relaying = val
267
+ }
268
+ }
269
+ get relaying() {
270
+ if (this.transaction && '_relaying' in this.transaction) return this.transaction._relaying
271
+ return this._relaying
272
+ }
273
+ process_line(line) {
274
+ if (this.state >= states.DISCONNECTING) {
275
+ if (logger.would_log(logger.LOGPROTOCOL)) {
276
+ this.logprotocol(`C: (after-disconnect): ${this.current_line}`, {
277
+ state: this.state,
278
+ })
279
+ }
280
+ this.loginfo(`data after disconnect from ${this.remote.ip}`)
281
+ return
282
+ }
283
+
284
+ if (this.state === states.DATA) {
285
+ if (logger.would_log(logger.LOGDATA)) {
286
+ this.logdata(`C: ${line}`)
287
+ }
288
+ this.accumulate_data(line)
289
+ return
290
+ }
291
+
292
+ this.current_line = line.toString(this.encoding).replace(/\r?\n/, '')
293
+ if (logger.would_log(logger.LOGPROTOCOL)) {
294
+ this.logprotocol(`C: ${this.current_line}`, { state: this.state })
295
+ }
296
+
297
+ // Check for non-ASCII characters
298
+ /* eslint no-control-regex: 0 */
299
+ if (/[^\x00-\x7F]/.test(this.current_line)) {
300
+ // See if this is a TLS handshake
301
+ const buf = Buffer.from(this.current_line.slice(0, 3), 'binary')
302
+ if (
303
+ buf[0] === 0x16 &&
304
+ buf[1] === 0x03 &&
305
+ (buf[2] === 0x00 || buf[2] === 0x01) // SSLv3/TLS1.x format
306
+ ) {
307
+ // Nuke the current input buffer to prevent processing further input
308
+ this.current_data = null
309
+ this.respond(501, 'SSL attempted over a non-SSL socket')
310
+ this.disconnect()
311
+ return
312
+ } else if (this.hello.verb == 'HELO') {
313
+ return this.respond(501, 'Syntax error (8-bit characters not allowed)')
314
+ }
315
+ }
316
+
317
+ if (this.state === states.CMD) {
318
+ this.state = states.PAUSE_SMTP
319
+ const matches = /^([^ ]*)( +(.*))?$/.exec(this.current_line)
320
+ if (!matches) {
321
+ return plugins.run_hooks('unrecognized_command', this, [this.current_line])
322
+ }
323
+ const cmd = matches[1]
324
+ const method = `cmd_${cmd.toLowerCase()}`
325
+ const remaining = matches[3] || ''
326
+ if (this[method]) {
327
+ try {
328
+ this[method](remaining)
329
+ } catch (err) {
330
+ if (err.stack) {
331
+ this.logerror(`${method} failed: ${err}`)
332
+ for (const line of err.stack.split('\n')) this.logerror(line)
333
+ } else {
334
+ this.logerror(`${method} failed: ${err}`)
335
+ }
336
+ this.respond(421, 'Internal Server Error', () => {
337
+ this.disconnect()
338
+ })
339
+ }
340
+ } else {
341
+ // unrecognized command
342
+ plugins.run_hooks('unrecognized_command', this, [cmd, remaining])
343
+ }
344
+ } else if (this.state === states.LOOP) {
345
+ // Allow QUIT
346
+ if (this.current_line.toUpperCase() === 'QUIT') {
347
+ this.state = states.PAUSE_SMTP
348
+ this.cmd_quit()
349
+ } else {
350
+ this.respond(this.loop_code, this.loop_msg)
351
+ }
352
+ } else if (this.state === states.PAUSE_SMTP) {
353
+ // Do nothing
354
+ } else {
355
+ throw new Error(`unknown state ${this.state}`)
356
+ }
357
+ }
358
+ process_data(data) {
359
+ if (this.state >= states.DISCONNECTING) {
360
+ this.loginfo(`data after disconnect from ${this.remote.ip}`)
361
+ return
362
+ }
363
+
364
+ if (!this.current_data || !this.current_data.length) {
365
+ this.current_data = data
366
+ } else {
367
+ // Data left over in buffer
368
+ const buf = Buffer.concat([this.current_data, data], this.current_data.length + data.length)
369
+ this.current_data = buf
370
+ }
371
+
372
+ this._process_data()
373
+ }
374
+ _process_data() {
375
+ // We *must* detect disconnected connections here as the state
376
+ // only transitions to states.CMD in the respond function below.
377
+ // Otherwise if multiple commands are pipelined and then the
378
+ // connection is dropped; we'll end up in the function forever.
379
+ if (this.state >= states.DISCONNECTING) return
380
+
381
+ let maxlength
382
+ if (this.state === states.PAUSE_DATA || this.state === states.DATA) {
383
+ maxlength = cfg.max.data_line_length
384
+ } else {
385
+ maxlength = cfg.max.line_length
386
+ }
387
+
388
+ let offset
389
+ while (this.current_data && (offset = utils.indexOfLF(this.current_data, maxlength)) !== -1) {
390
+ if (this.state === states.PAUSE_DATA) {
391
+ return
392
+ }
393
+ let this_line = this.current_data.slice(0, offset + 1)
394
+ // Hack: bypass this code to allow HAProxy's PROXY extension
395
+ const proxyStart = this.proxy.allowed && /^PROXY /.test(this_line)
396
+ if (this.state === states.PAUSE && proxyStart) {
397
+ if (this.proxy.timer) clearTimeout(this.proxy.timer)
398
+ this.state = states.CMD
399
+ this.current_data = this.current_data.slice(this_line.length)
400
+ this.process_line(this_line)
401
+ }
402
+ // Detect early_talker but allow PIPELINING extension (ESMTP)
403
+ else if ((this.state === states.PAUSE || this.state === states.PAUSE_SMTP) && !this.esmtp) {
404
+ // Allow EHLO/HELO to be pipelined with PROXY
405
+ if (this.proxy.allowed && /^(?:EH|HE)LO /i.test(this_line)) return
406
+ if (!this.early_talker) {
407
+ this_line = this_line.toString().replace(/\r?\n/, '')
408
+ this.logdebug('[early_talker]', {
409
+ state: this.state,
410
+ esmtp: this.esmtp,
411
+ line: this_line,
412
+ })
413
+ }
414
+ this.early_talker = true
415
+ setImmediate(() => {
416
+ this._process_data()
417
+ })
418
+ break
419
+ } else if ((this.state === states.PAUSE || this.state === states.PAUSE_SMTP) && this.esmtp) {
420
+ let valid = true
421
+ const cmd = this_line.toString('ascii').slice(0, 4).toUpperCase()
422
+ switch (cmd) {
423
+ case 'RSET':
424
+ case 'MAIL':
425
+ case 'SEND':
426
+ case 'SOML':
427
+ case 'SAML':
428
+ case 'RCPT':
429
+ // These can be anywhere in the group
430
+ break
431
+ default:
432
+ // Anything else *MUST* be the last command in the group
433
+ if (this_line.length !== this.current_data.length) {
434
+ valid = false
435
+ }
436
+ break
437
+ }
438
+ if (valid) {
439
+ // Valid PIPELINING
440
+ // We *don't want to process this yet otherwise the
441
+ // current_data buffer will be lost. The respond()
442
+ // function will call this function again once it
443
+ // has reset the state back to states.CMD and this
444
+ // ensures that we only process one command at a
445
+ // time.
446
+ this.pipelining = true
447
+ this.logdebug(`pipeline: ${this_line}`)
448
+ } else {
449
+ // Invalid pipeline sequence
450
+ // Treat this as early talker
451
+ if (!this.early_talker) {
452
+ this.logdebug('[early_talker]', {
453
+ state: this.state,
454
+ esmtp: this.esmtp,
455
+ line: this_line,
456
+ })
457
+ }
458
+ this.early_talker = true
459
+ setImmediate(() => {
460
+ this._process_data()
461
+ })
462
+ }
463
+ break
464
+ } else {
465
+ this.current_data = this.current_data.slice(this_line.length)
466
+ this.process_line(this_line)
467
+ }
468
+ }
469
+
470
+ if (
471
+ this.current_data &&
472
+ this.current_data.length > maxlength &&
473
+ utils.indexOfLF(this.current_data, maxlength) === -1
474
+ ) {
475
+ if (this.state !== states.DATA && this.state !== states.PAUSE_DATA) {
476
+ // In command mode, reject:
477
+ this.client.pause()
478
+ this.current_data = null
479
+ return this.respond(521, 'Command line too long', () => {
480
+ this.disconnect()
481
+ })
482
+ } else {
483
+ this.loginfo(`DATA line length (${this.current_data.length}) exceeds limit of ${maxlength} bytes`)
484
+ this.transaction.notes.data_line_length_exceeded = true
485
+ const b = Buffer.concat(
486
+ [
487
+ this.current_data.slice(0, maxlength - 2),
488
+ Buffer.from('\r\n ', 'utf8'),
489
+ this.current_data.slice(maxlength - 2),
490
+ ],
491
+ this.current_data.length + 3,
492
+ )
493
+ this.current_data = b
494
+ return this._process_data()
495
+ }
496
+ }
497
+ }
498
+ respond(code, msg, func) {
499
+ let uuid = ''
500
+ let messages
501
+
502
+ if (this.state === states.DISCONNECTED) {
503
+ if (func) func()
504
+ return
505
+ }
506
+ // Check to see if DSN object was passed in
507
+ if (typeof msg === 'object' && msg.constructor.name === 'DSN') {
508
+ // Override
509
+ code = msg.code
510
+ msg = msg.reply
511
+ }
512
+
513
+ if (!Array.isArray(msg)) {
514
+ messages = msg.toString().split(/\n/)
515
+ } else {
516
+ messages = msg.slice()
517
+ }
518
+ messages = messages.filter((msg2) => /\S/.test(msg2))
519
+
520
+ // Multiline AUTH PLAIN as in RFC-4954 page 8.
521
+ if (code === 334 && !messages.length) {
522
+ messages = [' ']
523
+ }
524
+
525
+ if (code >= 400) {
526
+ this.last_reject = `${code} ${messages.join(' ')}`
527
+ if (cfg.uuid.deny_chars) {
528
+ uuid = (this.transaction || this).uuid
529
+ if (cfg.uuid.deny_chars > 1) {
530
+ uuid = uuid.slice(0, cfg.uuid.deny_chars)
531
+ }
532
+ }
533
+ }
534
+
535
+ let mess
536
+ let buf = ''
537
+ const hostname = os.hostname().split('.').shift()
538
+ const _uuid = uuid ? `[${uuid}@${hostname}] ` : ''
539
+
540
+ while ((mess = messages.shift())) {
541
+ const line = `${code}${messages.length ? '-' : ' '}${_uuid}${mess}`
542
+ this.logprotocol(`S: ${line}`)
543
+ buf = `${buf}${line}\r\n`
544
+ }
545
+
546
+ if (this.client.write === undefined) return buf // testing
547
+
548
+ try {
549
+ this.client.write(buf)
550
+ } catch (err) {
551
+ return this.fail(`Writing response: ${buf} failed: ${err}`)
552
+ }
553
+
554
+ // Store the last response
555
+ this.last_response = buf
556
+
557
+ // Don't change loop state
558
+ if (this.state !== states.LOOP) {
559
+ this.state = states.CMD
560
+ }
561
+
562
+ // Run optional closure before handling and further commands
563
+ if (func) func()
564
+
565
+ // Process any buffered commands (PIPELINING)
566
+ this._process_data()
567
+ }
568
+ fail(err, err_data) {
569
+ if (err) this.logwarn(err, err_data)
570
+ this.hooks_to_run = []
571
+ this.disconnect()
572
+ }
573
+ disconnect() {
574
+ if (this.state >= states.DISCONNECTING) return
575
+ this.state = states.DISCONNECTING
576
+ this.current_data = null // don't process any more data we have already received
577
+ this.reset_transaction(() => {
578
+ plugins.run_hooks('disconnect', this)
579
+ })
580
+ }
581
+ disconnect_respond() {
582
+ const logdetail = {
583
+ ip: this.remote.ip,
584
+ rdns: this.remote.host ? this.remote.host : '',
585
+ helo: this.hello.host ? this.hello.host : '',
586
+ relay: this.relaying ? 'Y' : 'N',
587
+ early: this.early_talker ? 'Y' : 'N',
588
+ esmtp: this.esmtp ? 'Y' : 'N',
589
+ tls: this.tls.enabled ? 'Y' : 'N',
590
+ pipe: this.pipelining ? 'Y' : 'N',
591
+ errors: this.errors,
592
+ txns: this.tran_count,
593
+ rcpts: `${this.rcpt_count.accept}/${this.rcpt_count.tempfail}/${this.rcpt_count.reject}`,
594
+ msgs: `${this.msg_count.accept}/${this.msg_count.tempfail}/${this.msg_count.reject}`,
595
+ bytes: this.totalbytes,
596
+ lr: this.last_reject ? this.last_reject : '',
597
+ time: (Date.now() - this.start_time) / 1000,
598
+ }
599
+
600
+ this.results.add(
601
+ { name: 'disconnect' },
602
+ {
603
+ duration: (Date.now() - this.start_time) / 1000,
604
+ },
605
+ )
606
+ this.lognotice('disconnect', logdetail)
607
+ this.state = states.DISCONNECTED
608
+ this.client.end()
609
+ }
610
+ get_capabilities() {
611
+ return []
612
+ }
613
+ tran_uuid() {
614
+ this.tran_count++
615
+ return `${this.uuid}.${this.tran_count}`
616
+ }
617
+ reset_transaction(cb) {
618
+ this.results.add(
619
+ { name: 'reset' },
620
+ {
621
+ duration: (Date.now() - this.start_time) / 1000,
622
+ },
623
+ )
624
+ if (this.transaction && this.transaction.resetting === false) {
625
+ // Pause connection to allow the hook to complete
626
+ this.pause()
627
+ this.transaction.resetting = true
628
+ plugins.run_hooks('reset_transaction', this, cb)
629
+ } else {
630
+ this.transaction = null
631
+ if (cb) cb()
632
+ }
633
+ }
634
+ reset_transaction_respond(retval, msg, cb) {
635
+ if (this.transaction) {
636
+ this.transaction.message_stream.destroy()
637
+ this.transaction = null
638
+ }
639
+ if (cb) cb()
640
+ // Allow the connection to continue
641
+ this.resume()
642
+ }
643
+ init_transaction(cb) {
644
+ this.reset_transaction(() => {
645
+ this.transaction = trans.createTransaction(this.tran_uuid(), cfg)
646
+ // Catch any errors from the message_stream
647
+ this.transaction.message_stream.on('error', (err) => {
648
+ this.logcrit(`message_stream error: ${err.message}`)
649
+ this.respond('421', 'Internal Server Error', () => {
650
+ this.disconnect()
651
+ })
652
+ })
653
+ this.transaction.results = new ResultStore(this)
654
+ if (cb) cb()
655
+ })
656
+ }
657
+ loop_respond(code, msg) {
658
+ if (this.state >= states.DISCONNECTING) return
659
+ this.state = states.LOOP
660
+ this.loop_code = code
661
+ this.loop_msg = msg
662
+ this.respond(code, msg)
663
+ }
664
+ pause() {
665
+ if (this.state >= states.DISCONNECTING) return
666
+ this.client.pause()
667
+ if (this.state !== states.PAUSE_DATA) this.prev_state = this.state
668
+ this.state = states.PAUSE_DATA
669
+ }
670
+ resume() {
671
+ if (this.state >= states.DISCONNECTING) return
672
+ this.client.resume()
673
+ if (this.prev_state && this.state === states.PAUSE_DATA) {
674
+ this.state = this.prev_state
675
+ }
676
+ this.prev_state = null
677
+ setImmediate(() => this._process_data())
678
+ }
679
+ /////////////////////////////////////////////////////////////////////////////
680
+ // SMTP Responses
681
+ connect_init_respond() {
682
+ this.logdebug('running connect_init_respond')
683
+ plugins.run_hooks('lookup_rdns', this)
684
+ }
685
+ lookup_rdns_respond(retval, msg) {
686
+ switch (retval) {
687
+ case constants.ok:
688
+ this.set('remote', 'host', msg || 'Unknown')
689
+ this.set('remote', 'info', this.remote.info || this.remote.host)
690
+ plugins.run_hooks('connect', this)
691
+ break
692
+ case constants.deny:
693
+ this.loop_respond(554, msg || 'rDNS Lookup Failed')
694
+ break
695
+ case constants.denydisconnect:
696
+ case constants.disconnect:
697
+ this.respond(554, msg || 'rDNS Lookup Failed', () => {
698
+ this.disconnect()
699
+ })
700
+ break
701
+ case constants.denysoft:
702
+ this.loop_respond(421, msg || 'rDNS Temporary Failure')
703
+ break
704
+ case constants.denysoftdisconnect:
705
+ this.respond(421, msg || 'rDNS Temporary Failure', () => {
706
+ this.disconnect()
707
+ })
708
+ break
709
+ default:
710
+ // BUG: dns.reverse throws on invalid input (and sometimes valid
711
+ // input nodejs/node#47847). Also throws when empty results
712
+ try {
713
+ dns.reverse(this.remote.ip, (err, domains) => {
714
+ this.rdns_response(err, domains)
715
+ })
716
+ } catch (err) {
717
+ this.rdns_response(err, [])
718
+ }
719
+ }
720
+ }
721
+ rdns_response(err, domains) {
722
+ if (err) {
723
+ switch (err.code) {
724
+ case dns.NXDOMAIN:
725
+ case dns.NOTFOUND:
726
+ this.set('remote', 'host', 'NXDOMAIN')
727
+ break
728
+ default:
729
+ this.set('remote', 'host', 'DNSERROR')
730
+ break
731
+ }
732
+ } else {
733
+ this.set('remote', 'host', domains[0] || 'Unknown')
734
+ this.results.add({ name: 'remote' }, this.remote)
735
+ }
736
+ this.set('remote', 'info', this.remote.info || this.remote.host)
737
+ plugins.run_hooks('connect', this)
738
+ }
739
+ unrecognized_command_respond(retval, msg) {
740
+ switch (retval) {
741
+ case constants.ok:
742
+ // response already sent, cool...
743
+ break
744
+ case constants.next_hook:
745
+ plugins.run_hooks(msg, this)
746
+ break
747
+ case constants.deny:
748
+ this.respond(500, msg || 'Unrecognized command')
749
+ break
750
+ case constants.denydisconnect:
751
+ case constants.denysoftdisconnect:
752
+ this.respond(retval === constants.denydisconnect ? 521 : 421, msg || 'Unrecognized command', () => {
753
+ this.disconnect()
754
+ })
755
+ break
756
+ default:
757
+ this.errors++
758
+ this.respond(500, msg || 'Unrecognized command')
759
+ }
760
+ }
761
+ connect_respond(retval, msg) {
762
+ // RFC 5321 Section 4.3.2 states that the only valid SMTP codes here are:
763
+ // 220 = Service ready
764
+ // 554 = Transaction failed (no SMTP service here)
765
+ // 421 = Service shutting down and closing transmission channel
766
+ switch (retval) {
767
+ case constants.deny:
768
+ this.loop_respond(554, msg || 'Your mail is not welcome here')
769
+ break
770
+ case constants.denydisconnect:
771
+ case constants.disconnect:
772
+ this.respond(554, msg || 'Your mail is not welcome here', () => {
773
+ this.disconnect()
774
+ })
775
+ break
776
+ case constants.denysoft:
777
+ this.loop_respond(421, msg || 'Come back later')
778
+ break
779
+ case constants.denysoftdisconnect:
780
+ this.respond(421, msg || 'Come back later', () => {
781
+ this.disconnect()
782
+ })
783
+ break
784
+ default: {
785
+ let greeting
786
+ if (cfg.message.greeting?.length) {
787
+ // RFC5321 section 4.2
788
+ // Hostname/domain should appear after the 220
789
+ greeting = [...cfg.message.greeting]
790
+ greeting[0] = `${this.local.host} ESMTP ${greeting[0]}`
791
+ if (cfg.uuid.banner_chars) {
792
+ greeting[0] += ` (${this.uuid.slice(0, cfg.uuid.banner_chars)})`
793
+ }
794
+ } else {
795
+ greeting = `${this.local.host} ESMTP ${this.local.info} ready`
796
+ if (cfg.uuid.banner_chars) {
797
+ greeting += ` (${this.uuid.slice(0, cfg.uuid.banner_chars)})`
798
+ }
799
+ }
800
+ this.respond(220, msg || greeting)
801
+ }
802
+ }
803
+ }
804
+ get_remote(prop) {
805
+ switch (this.remote[prop]) {
806
+ case 'NXDOMAIN':
807
+ case 'DNSERROR':
808
+ case '':
809
+ case undefined:
810
+ case null:
811
+ return `[${this.remote.ip}]`
812
+ default:
813
+ return `${this.remote[prop]} [${this.remote.ip}]`
814
+ }
815
+ }
816
+ helo_respond(retval, msg) {
817
+ switch (retval) {
818
+ case constants.deny:
819
+ this.respond(550, msg || 'HELO denied', () => {
820
+ this.set('hello', 'verb', null)
821
+ this.set('hello', 'host', null)
822
+ })
823
+ break
824
+ case constants.denydisconnect:
825
+ this.respond(550, msg || 'HELO denied', () => {
826
+ this.disconnect()
827
+ })
828
+ break
829
+ case constants.denysoft:
830
+ this.respond(450, msg || 'HELO denied', () => {
831
+ this.set('hello', 'verb', null)
832
+ this.set('hello', 'host', null)
833
+ })
834
+ break
835
+ case constants.denysoftdisconnect:
836
+ this.respond(450, msg || 'HELO denied', () => {
837
+ this.disconnect()
838
+ })
839
+ break
840
+ default:
841
+ // RFC5321 section 4.1.1.1
842
+ // Hostname/domain should appear after 250
843
+ this.respond(250, `${this.local.host} Hello ${this.get_remote('host')}, ${cfg.message.helo}`)
844
+ }
845
+ }
846
+ ehlo_respond(retval, msg) {
847
+ switch (retval) {
848
+ case constants.deny:
849
+ this.respond(550, msg || 'EHLO denied', () => {
850
+ this.set('hello', 'verb', null)
851
+ this.set('hello', 'host', null)
852
+ })
853
+ break
854
+ case constants.denydisconnect:
855
+ this.respond(550, msg || 'EHLO denied', () => {
856
+ this.disconnect()
857
+ })
858
+ break
859
+ case constants.denysoft:
860
+ this.respond(450, msg || 'EHLO denied', () => {
861
+ this.set('hello', 'verb', null)
862
+ this.set('hello', 'host', null)
863
+ })
864
+ break
865
+ case constants.denysoftdisconnect:
866
+ this.respond(450, msg || 'EHLO denied', () => {
867
+ this.disconnect()
868
+ })
869
+ break
870
+ default: {
871
+ // RFC5321 section 4.1.1.1
872
+ // Hostname/domain should appear after 250
873
+
874
+ const response = [
875
+ `${this.local.host} Hello ${this.get_remote('host')}, ${cfg.message.helo}`,
876
+ 'PIPELINING',
877
+ '8BITMIME',
878
+ ]
879
+
880
+ if (cfg.main.smtputf8) response.push('SMTPUTF8')
881
+
882
+ response.push(`SIZE ${cfg.max.bytes}`)
883
+
884
+ this.capabilities = response
885
+
886
+ plugins.run_hooks('capabilities', this)
887
+ this.esmtp = true
888
+ }
889
+ }
890
+ }
891
+ capabilities_respond() {
892
+ this.respond(250, this.capabilities)
893
+ }
894
+ quit_respond(retval, msg) {
895
+ this.respond(221, msg || `${this.local.host} ${cfg.message.close}`, () => {
896
+ this.disconnect()
897
+ })
898
+ }
899
+ vrfy_respond(retval, msg) {
900
+ switch (retval) {
901
+ case constants.deny:
902
+ this.respond(550, msg || 'Access Denied', () => {
903
+ this.reset_transaction()
904
+ })
905
+ break
906
+ case constants.denydisconnect:
907
+ this.respond(550, msg || 'Access Denied', () => {
908
+ this.disconnect()
909
+ })
910
+ break
911
+ case constants.denysoft:
912
+ this.respond(450, msg || 'Lookup Failed', () => {
913
+ this.reset_transaction()
914
+ })
915
+ break
916
+ case constants.denysoftdisconnect:
917
+ this.respond(450, msg || 'Lookup Failed', () => {
918
+ this.disconnect()
919
+ })
920
+ break
921
+ case constants.ok:
922
+ this.respond(250, msg || 'User OK')
923
+ break
924
+ default:
925
+ this.respond(252, "Just try sending a mail and we'll see how it turns out...")
926
+ }
927
+ }
928
+ noop_respond(retval, msg) {
929
+ switch (retval) {
930
+ case constants.deny:
931
+ this.respond(500, msg || 'Stop wasting my time')
932
+ break
933
+ case constants.denydisconnect:
934
+ this.respond(500, msg || 'Stop wasting my time', () => {
935
+ this.disconnect()
936
+ })
937
+ break
938
+ default:
939
+ this.respond(250, 'OK')
940
+ }
941
+ }
942
+ rset_respond() {
943
+ this.respond(250, 'OK', () => {
944
+ this.reset_transaction()
945
+ })
946
+ }
947
+ mail_respond(retval, msg) {
948
+ if (!this.transaction) {
949
+ this.logerror('mail_respond found no transaction!')
950
+ return
951
+ }
952
+ const sender = this.transaction.mail_from
953
+ const dmsg = `sender ${sender.format()}`
954
+ this.lognotice(dmsg, {
955
+ code: constants.translate(retval),
956
+ msg: msg || '',
957
+ })
958
+
959
+ const store_results = (action) => {
960
+ let addr = sender.format()
961
+ if (addr.length > 2) {
962
+ // all but null sender
963
+ addr = addr.slice(1, -1) // trim off < >
964
+ }
965
+ this.transaction.results.add(
966
+ { name: 'mail_from' },
967
+ {
968
+ action,
969
+ code: constants.translate(retval),
970
+ address: addr,
971
+ },
972
+ )
973
+ }
974
+
975
+ switch (retval) {
976
+ case constants.deny:
977
+ this.respond(550, msg || `${dmsg} denied`, () => {
978
+ store_results('reject')
979
+ this.reset_transaction()
980
+ })
981
+ break
982
+ case constants.denydisconnect:
983
+ this.respond(550, msg || `${dmsg} denied`, () => {
984
+ store_results('reject')
985
+ this.disconnect()
986
+ })
987
+ break
988
+ case constants.denysoft:
989
+ this.respond(450, msg || `${dmsg} denied`, () => {
990
+ store_results('tempfail')
991
+ this.reset_transaction()
992
+ })
993
+ break
994
+ case constants.denysoftdisconnect:
995
+ this.respond(450, msg || `${dmsg} denied`, () => {
996
+ store_results('tempfail')
997
+ this.disconnect()
998
+ })
999
+ break
1000
+ default:
1001
+ store_results('accept')
1002
+ this.respond(250, msg || `${dmsg} OK`)
1003
+ }
1004
+ }
1005
+ rcpt_incr(rcpt, action, msg, retval) {
1006
+ this.transaction.rcpt_count[action]++
1007
+ this.rcpt_count[action]++
1008
+
1009
+ const addr = rcpt.format()
1010
+ const recipient = {
1011
+ address: addr.slice(1, -1),
1012
+ action,
1013
+ }
1014
+
1015
+ if (msg && action !== 'accept') {
1016
+ if (typeof msg === 'object' && msg.constructor.name === 'DSN') {
1017
+ recipient.msg = msg.reply
1018
+ recipient.code = msg.code
1019
+ } else {
1020
+ recipient.msg = msg
1021
+ recipient.code = constants.translate(retval)
1022
+ }
1023
+ }
1024
+
1025
+ this.transaction.results.push({ name: 'rcpt_to' }, { recipient })
1026
+ }
1027
+ rcpt_ok_respond(retval, msg) {
1028
+ if (!this.transaction) {
1029
+ this.results.add(this, {
1030
+ err: 'rcpt_ok_respond found no transaction',
1031
+ })
1032
+ return
1033
+ }
1034
+ if (!msg) msg = this.last_rcpt_msg
1035
+ const rcpt = this.transaction.rcpt_to[this.transaction.rcpt_to.length - 1]
1036
+ const dmsg = `recipient ${rcpt.format()}`
1037
+ // Log OK instead of CONT as this hook only runs if hook_rcpt returns OK
1038
+ this.lognotice(dmsg, {
1039
+ code: constants.translate(retval === constants.cont ? constants.ok : retval),
1040
+ msg: msg || '',
1041
+ sender: this.transaction.mail_from.address,
1042
+ })
1043
+ switch (retval) {
1044
+ case constants.deny:
1045
+ this.respond(550, msg || `${dmsg} denied`, () => {
1046
+ this.rcpt_incr(rcpt, 'reject', msg, retval)
1047
+ this.transaction.rcpt_to.pop()
1048
+ })
1049
+ break
1050
+ case constants.denydisconnect:
1051
+ this.respond(550, msg || `${dmsg} denied`, () => {
1052
+ this.rcpt_incr(rcpt, 'reject', msg, retval)
1053
+ this.disconnect()
1054
+ })
1055
+ break
1056
+ case constants.denysoft:
1057
+ this.respond(450, msg || `${dmsg} denied`, () => {
1058
+ this.rcpt_incr(rcpt, 'tempfail', msg, retval)
1059
+ this.transaction.rcpt_to.pop()
1060
+ })
1061
+ break
1062
+ case constants.denysoftdisconnect:
1063
+ this.respond(450, msg || `${dmsg} denied`, () => {
1064
+ this.rcpt_incr(rcpt, 'tempfail', msg, retval)
1065
+ this.disconnect()
1066
+ })
1067
+ break
1068
+ default:
1069
+ this.respond(250, msg || `${dmsg} OK`, () => {
1070
+ this.rcpt_incr(rcpt, 'accept', msg, retval)
1071
+ })
1072
+ }
1073
+ }
1074
+ rcpt_respond(retval, msg) {
1075
+ if (retval === constants.cont && this.relaying) {
1076
+ retval = constants.ok
1077
+ }
1078
+
1079
+ if (!this.transaction) {
1080
+ this.results.add(this, {
1081
+ err: 'rcpt_respond found no transaction',
1082
+ })
1083
+ return
1084
+ }
1085
+ const rcpt = this.transaction.rcpt_to[this.transaction.rcpt_to.length - 1]
1086
+ const dmsg = `recipient ${rcpt.format()}`
1087
+ if (retval !== constants.ok) {
1088
+ this.lognotice(dmsg, {
1089
+ code: constants.translate(retval === constants.cont ? constants.ok : retval),
1090
+ msg: msg || '',
1091
+ sender: this.transaction.mail_from.address,
1092
+ })
1093
+ }
1094
+ switch (retval) {
1095
+ case constants.deny:
1096
+ this.respond(550, msg || `${dmsg} denied`, () => {
1097
+ this.rcpt_incr(rcpt, 'reject', msg, retval)
1098
+ this.transaction.rcpt_to.pop()
1099
+ })
1100
+ break
1101
+ case constants.denydisconnect:
1102
+ this.respond(550, msg || `${dmsg} denied`, () => {
1103
+ this.rcpt_incr(rcpt, 'reject', msg, retval)
1104
+ this.disconnect()
1105
+ })
1106
+ break
1107
+ case constants.denysoft:
1108
+ this.respond(450, msg || `${dmsg} denied`, () => {
1109
+ this.rcpt_incr(rcpt, 'tempfail', msg, retval)
1110
+ this.transaction.rcpt_to.pop()
1111
+ })
1112
+ break
1113
+ case constants.denysoftdisconnect:
1114
+ this.respond(450, msg || `${dmsg} denied`, () => {
1115
+ this.rcpt_incr(rcpt, 'tempfail', msg, retval)
1116
+ this.disconnect()
1117
+ })
1118
+ break
1119
+ case constants.ok:
1120
+ // Store any msg for rcpt_ok
1121
+ this.last_rcpt_msg = msg
1122
+ plugins.run_hooks('rcpt_ok', this, rcpt)
1123
+ break
1124
+ default: {
1125
+ if (retval !== constants.cont) {
1126
+ this.logalert('No plugin determined if relaying was allowed')
1127
+ }
1128
+ const rej_msg = `I cannot deliver mail for ${rcpt.format()}`
1129
+ this.respond(550, rej_msg, () => {
1130
+ this.rcpt_incr(rcpt, 'reject', rej_msg, retval)
1131
+ this.transaction.rcpt_to.pop()
1132
+ })
1133
+ }
1134
+ }
1135
+ }
1136
+ /////////////////////////////////////////////////////////////////////////////
1137
+ // HAProxy support
1138
+
1139
+ apply_proxy(proxy) {
1140
+ if (this.proxy.timer) {
1141
+ clearTimeout(this.proxy.timer)
1142
+ this.proxy.timer = null
1143
+ }
1144
+
1145
+ const { proto, src_ip, src_port, dst_ip, dst_port } = proxy
1146
+ const proxy_ip = proxy.proxy_ip || this.remote.ip
1147
+
1148
+ // Apply changes
1149
+ this.loginfo('HAProxy', {
1150
+ proto,
1151
+ src_ip: `${src_ip}:${src_port}`,
1152
+ dst_ip: `${dst_ip}:${dst_port}`,
1153
+ })
1154
+
1155
+ this.notes.proxy = {
1156
+ type: 'haproxy',
1157
+ proto,
1158
+ src_ip,
1159
+ src_port,
1160
+ dst_ip,
1161
+ dst_port,
1162
+ proxy_ip,
1163
+ }
1164
+
1165
+ this.reset_transaction(() => {
1166
+ this.set('proxy.ip', proxy_ip)
1167
+ this.set('proxy.type', 'haproxy')
1168
+ this.relaying = false
1169
+ this.set('local.ip', dst_ip)
1170
+ this.set('local.port', parseInt(dst_port, 10))
1171
+ this.set('remote.ip', src_ip)
1172
+ this.set('remote.port', parseInt(src_port, 10))
1173
+ this.set('remote.host', null)
1174
+ this.set('hello.host', null)
1175
+ this.results.add({ name: 'local' }, this.local)
1176
+ this.results.add({ name: 'remote' }, this.remote)
1177
+ plugins.run_hooks('connect_init', this)
1178
+ })
1179
+ }
1180
+
1181
+ cmd_proxy(line) {
1182
+ if (!this.proxy.allowed) {
1183
+ this.respond(421, `PROXY not allowed from ${this.remote.ip}`)
1184
+ return this.disconnect()
1185
+ }
1186
+
1187
+ const proxy = net_utils.parse_proxy_line(line)
1188
+ if (!proxy) {
1189
+ this.respond(421, 'Invalid PROXY format')
1190
+ return this.disconnect()
1191
+ }
1192
+
1193
+ this.apply_proxy(proxy)
1194
+ }
1195
+ /////////////////////////////////////////////////////////////////////////////
1196
+ // SMTP Commands
1197
+
1198
+ cmd_internalcmd(line) {
1199
+ if (!this.remote.is_local) {
1200
+ return this.respond(501, 'INTERNALCMD not allowed remotely')
1201
+ }
1202
+ const results = String(line).split(/ +/)
1203
+ if (/key:/.test(results[0])) {
1204
+ const internal_key = config.get('internalcmd_key')
1205
+ if (results[0] != `key:${internal_key}`) {
1206
+ return this.respond(501, 'Invalid internalcmd_key - check config')
1207
+ }
1208
+ results.shift()
1209
+ } else if (config.get('internalcmd_key')) {
1210
+ return this.respond(501, 'Missing internalcmd_key - check config')
1211
+ }
1212
+
1213
+ // Now send the internal command to the master process
1214
+ const command = results.shift()
1215
+ if (!command) {
1216
+ return this.respond(501, 'No command given')
1217
+ }
1218
+
1219
+ require('./server').sendToMaster(command, results)
1220
+ return this.respond(250, 'Command sent for execution. Check Haraka logs for results.')
1221
+ }
1222
+ cmd_helo(line) {
1223
+ const results = String(line).split(/ +/)
1224
+ const host = results[0]
1225
+ if (!host) {
1226
+ return this.respond(501, 'HELO requires domain/address - see RFC-2821 4.1.1.1')
1227
+ }
1228
+ // RFC 5321 §4.1.1.1: the domain/address-literal cannot contain
1229
+ // control characters. process_line() only strips the first \r?\n,
1230
+ // so a bare \r could otherwise survive into hello.host and the
1231
+ // generated Received: header / logs (header injection).
1232
+ if (/[\x00-\x1f\x7f]/.test(host)) {
1233
+ return this.respond(501, 'HELO syntax error - see RFC-2821 4.1.1.1')
1234
+ }
1235
+
1236
+ this.reset_transaction(() => {
1237
+ this.set('hello', 'verb', 'HELO')
1238
+ this.set('hello', 'host', host)
1239
+ this.results.add({ name: 'helo' }, this.hello)
1240
+ plugins.run_hooks('helo', this, host)
1241
+ })
1242
+ }
1243
+ cmd_ehlo(line) {
1244
+ const results = String(line).split(/ +/)
1245
+ const host = results[0]
1246
+ if (!host) {
1247
+ return this.respond(501, 'EHLO requires domain/address - see RFC-2821 4.1.1.1')
1248
+ }
1249
+ // RFC 5321 §4.1.1.1: reject control chars (see cmd_helo).
1250
+ if (/[\x00-\x1f\x7f]/.test(host)) {
1251
+ return this.respond(501, 'EHLO syntax error - see RFC-2821 4.1.1.1')
1252
+ }
1253
+
1254
+ this.reset_transaction(() => {
1255
+ this.set('hello', 'verb', 'EHLO')
1256
+ this.set('hello', 'host', host)
1257
+ this.results.add({ name: 'helo' }, this.hello)
1258
+ plugins.run_hooks('ehlo', this, host)
1259
+ })
1260
+ }
1261
+ cmd_quit(args) {
1262
+ // RFC 5321 Section 4.3.2
1263
+ // QUIT does not accept arguments
1264
+ if (args) {
1265
+ return this.respond(501, 'Syntax error')
1266
+ }
1267
+ plugins.run_hooks('quit', this)
1268
+ }
1269
+ cmd_rset(args) {
1270
+ // RFC 5321 Section 4.3.2
1271
+ // RSET does not accept arguments
1272
+ if (args) {
1273
+ return this.respond(501, 'Syntax error')
1274
+ }
1275
+ plugins.run_hooks('rset', this)
1276
+ }
1277
+ cmd_vrfy() {
1278
+ // only supported via plugins
1279
+ plugins.run_hooks('vrfy', this)
1280
+ }
1281
+ cmd_noop() {
1282
+ plugins.run_hooks('noop', this)
1283
+ }
1284
+ cmd_help() {
1285
+ this.respond(250, 'Not implemented')
1286
+ }
1287
+ cmd_mail(line) {
1288
+ if (!this.hello.host) {
1289
+ this.errors++
1290
+ return this.respond(503, 'Use EHLO/HELO before MAIL')
1291
+ }
1292
+ // Require authentication on ports 587 & 465
1293
+ if (!this.relaying && [587, 465].includes(this.local.port)) {
1294
+ this.errors++
1295
+ return this.respond(550, 'Authentication required')
1296
+ }
1297
+
1298
+ let results
1299
+ try {
1300
+ results = rfc1869.parse('mail', line, !this.relaying && cfg.main.strict_rfc1869)
1301
+ } catch (err) {
1302
+ this.errors++
1303
+ if (err.stack) {
1304
+ this.lognotice(err.stack.split(/\n/)[0])
1305
+ } else {
1306
+ this.logerror(err)
1307
+ }
1308
+ // Explicitly handle out-of-disk space errors
1309
+ if (err.code === 'ENOSPC') {
1310
+ return this.respond(452, 'Internal Server Error')
1311
+ } else {
1312
+ return this.respond(501, ['Command parsing failed', err])
1313
+ }
1314
+ }
1315
+
1316
+ let from
1317
+ const from_raw = results.shift()
1318
+ try {
1319
+ from = new Address(from_raw, { postel: cfg.main.postel })
1320
+ } catch (err) {
1321
+ const msg = `Invalid MAIL FROM address ${utils.sanitize(from_raw)}: ${err.message}`
1322
+ this.lognotice(msg)
1323
+ return this.respond(501, msg)
1324
+ }
1325
+
1326
+ // Get rest of key=value pairs
1327
+ const params = {}
1328
+ for (const param of results) {
1329
+ const kv = param.match(/^([^=]+)(?:=(.+))?$/)
1330
+ if (kv) params[kv[1].toUpperCase()] = kv[2] || null
1331
+ }
1332
+
1333
+ // Parameters are only valid if EHLO was sent
1334
+ if (!this.esmtp && Object.keys(params).length > 0) {
1335
+ return this.respond(555, 'Invalid command parameters')
1336
+ }
1337
+
1338
+ // Handle SIZE extension
1339
+ if (params?.SIZE && params.SIZE > 0) {
1340
+ if (cfg.max.bytes > 0 && params.SIZE > cfg.max.bytes) {
1341
+ return this.respond(550, 'Message too big!')
1342
+ }
1343
+ }
1344
+
1345
+ this.init_transaction(() => {
1346
+ this.transaction.mail_from = from
1347
+ if (this.hello.verb == 'HELO') {
1348
+ this.transaction.encoding = 'binary'
1349
+ this.encoding = 'binary'
1350
+ }
1351
+ plugins.run_hooks('mail', this, [from, params])
1352
+ })
1353
+ }
1354
+ cmd_rcpt(line) {
1355
+ if (!this.transaction || !this.transaction.mail_from) {
1356
+ this.errors++
1357
+ return this.respond(503, 'Use MAIL before RCPT')
1358
+ }
1359
+
1360
+ let results
1361
+ try {
1362
+ results = rfc1869.parse('rcpt', line, cfg.main.strict_rfc1869 && !this.relaying)
1363
+ } catch (err) {
1364
+ this.errors++
1365
+ if (err.stack) {
1366
+ this.lognotice(err.stack.split(/\n/)[0])
1367
+ } else {
1368
+ this.logerror(err)
1369
+ }
1370
+ // Explicitly handle out-of-disk space errors
1371
+ if (err.code === 'ENOSPC') {
1372
+ return this.respond(452, 'Internal Server Error')
1373
+ } else {
1374
+ return this.respond(501, ['Command parsing failed', err])
1375
+ }
1376
+ }
1377
+
1378
+ let recip
1379
+ const recip_raw = results.shift()
1380
+ try {
1381
+ recip = new Address(recip_raw, { postel: cfg.main.postel })
1382
+ } catch (err) {
1383
+ const msg = `Invalid RCPT TO address ${utils.sanitize(recip_raw)}: ${err.message}`
1384
+ this.lognotice(msg)
1385
+ return this.respond(501, msg)
1386
+ }
1387
+
1388
+ // Get rest of key=value pairs
1389
+ const params = {}
1390
+ for (const param of results) {
1391
+ const kv = param.match(/^([^=]+)(?:=(.+))?$/)
1392
+ if (kv) params[kv[1].toUpperCase()] = kv[2] || null
1393
+ }
1394
+
1395
+ // Parameters are only valid if EHLO was sent
1396
+ if (!this.esmtp && Object.keys(params).length > 0) {
1397
+ return this.respond(555, 'Invalid command parameters')
1398
+ }
1399
+
1400
+ this.transaction.rcpt_to.push(recip)
1401
+ plugins.run_hooks('rcpt', this, [recip, params])
1402
+ }
1403
+ received_line() {
1404
+ let smtp = this.hello.verb === 'EHLO' ? 'ESMTP' : 'SMTP'
1405
+ // Implement RFC3848
1406
+ if (this.tls.enabled) smtp += 'S'
1407
+ if (this.authheader) smtp += 'A'
1408
+
1409
+ let sslheader
1410
+
1411
+ if (this.get('tls.cipher.version')) {
1412
+ // standardName appeared in Node.js v12.16 and v13.4
1413
+ // RFC 8314
1414
+ sslheader = `tls ${this.tls.cipher.standardName || this.tls.cipher.name}`
1415
+ }
1416
+
1417
+ let received_header = `from ${this.hello.host} (${this.get_remote('info')})\r
1418
+ \tby ${this.local.host} (${this.local.info}) with ${smtp} id ${this.transaction.uuid}\r
1419
+ \tenvelope-from ${this.transaction.mail_from.format()}`
1420
+
1421
+ if (sslheader) received_header += `\r\n\t${sslheader.replace(/\r?\n\t?$/, '')}`
1422
+
1423
+ // Does not follow RFC 5321 section 4.4 grammar
1424
+ if (this.authheader) received_header += ` ${this.authheader.replace(/\r?\n\t?$/, '')}`
1425
+
1426
+ received_header += `;\r\n\t${utils.date_to_str(new Date())}`
1427
+
1428
+ return received_header
1429
+ }
1430
+ auth_results(message) {
1431
+ // https://datatracker.ietf.org/doc/rfc7001/
1432
+ const has_tran = !!this.transaction?.notes
1433
+
1434
+ // initialize connection note
1435
+ if (!this.notes.authentication_results) {
1436
+ this.notes.authentication_results = []
1437
+ }
1438
+
1439
+ // initialize transaction note, if possible
1440
+ if (has_tran === true && !this.transaction.notes.authentication_results) {
1441
+ this.transaction.notes.authentication_results = []
1442
+ }
1443
+
1444
+ // Strip CR/LF and other control chars: an attacker-influenced
1445
+ // value (e.g. a failed AUTH username, see auth_base) must not be
1446
+ // able to inject extra header lines into Authentication-Results.
1447
+ // The legitimate folding (;\r\n\t) is added by the join below.
1448
+ // if message, store it in the appropriate note
1449
+ if (message) {
1450
+ if (has_tran === true) {
1451
+ this.transaction.notes.authentication_results.push(utils.sanitize(message))
1452
+ } else {
1453
+ this.notes.authentication_results.push(utils.sanitize(message))
1454
+ }
1455
+ }
1456
+
1457
+ // assemble the new header
1458
+ let header = [utils.sanitize(this.local.host), ...this.notes.authentication_results]
1459
+ if (has_tran === true) {
1460
+ header = [...header, ...this.transaction.notes.authentication_results]
1461
+ }
1462
+ if (header.length === 1) return '' // no results
1463
+ return header.join(';\r\n\t')
1464
+ }
1465
+ auth_results_clean() {
1466
+ // move any existing Auth-Res headers to Original-Auth-Res headers
1467
+ // http://tools.ietf.org/html/draft-kucherawy-original-authres-00.html
1468
+ const ars = this.transaction.header.get_all('Authentication-Results')
1469
+ if (ars.length === 0) return
1470
+
1471
+ for (const element of ars) {
1472
+ this.transaction.add_header('Original-Authentication-Results', element)
1473
+ }
1474
+ this.transaction.remove_header('Authentication-Results')
1475
+ this.logdebug('Authentication-Results moved to Original-Authentication-Results')
1476
+ }
1477
+ cmd_data(args) {
1478
+ // RFC 5321 Section 4.3.2
1479
+ // DATA does not accept arguments
1480
+ if (args) {
1481
+ this.errors++
1482
+ return this.respond(501, 'Syntax error')
1483
+ }
1484
+ if (!this.transaction) {
1485
+ this.errors++
1486
+ return this.respond(503, 'MAIL required first')
1487
+ }
1488
+ if (!this.transaction.rcpt_to.length) {
1489
+ if (this.pipelining) {
1490
+ return this.respond(554, 'No valid recipients')
1491
+ }
1492
+ this.errors++
1493
+ return this.respond(503, 'RCPT required first')
1494
+ }
1495
+
1496
+ if (cfg.headers.add_received) {
1497
+ this.accumulate_data(`Received: ${this.received_line()}\r\n`)
1498
+ }
1499
+ plugins.run_hooks('data', this)
1500
+ }
1501
+ data_respond(retval, msg) {
1502
+ let cont = 0
1503
+ switch (retval) {
1504
+ case constants.deny:
1505
+ this.respond(554, msg || 'Message denied', () => {
1506
+ this.reset_transaction()
1507
+ })
1508
+ break
1509
+ case constants.denydisconnect:
1510
+ this.respond(554, msg || 'Message denied', () => {
1511
+ this.disconnect()
1512
+ })
1513
+ break
1514
+ case constants.denysoft:
1515
+ this.respond(451, msg || 'Message denied', () => {
1516
+ this.reset_transaction()
1517
+ })
1518
+ break
1519
+ case constants.denysoftdisconnect:
1520
+ this.respond(451, msg || 'Message denied', () => {
1521
+ this.disconnect()
1522
+ })
1523
+ break
1524
+ default:
1525
+ cont = 1
1526
+ }
1527
+ if (!cont) return
1528
+
1529
+ // We already checked for MAIL/RCPT in cmd_data
1530
+ this.respond(354, 'go ahead, make my day', () => {
1531
+ // OK... now we get the data
1532
+ this.state = states.DATA
1533
+ this.transaction.data_bytes = 0
1534
+ })
1535
+ }
1536
+ accumulate_data(line) {
1537
+ this.transaction.data_bytes += line.length
1538
+
1539
+ // Look for .\r\n
1540
+ if (line.length === 3 && line[0] === 0x2e && line[1] === 0x0d && line[2] === 0x0a) {
1541
+ this.data_done()
1542
+ return
1543
+ }
1544
+
1545
+ // Look for .\n
1546
+ if (line.length === 2 && line[0] === 0x2e && line[1] === 0x0a) {
1547
+ this.lognotice('Client sent bare line-feed - .\\n rather than .\\r\\n')
1548
+ this.respond(451, 'Bare line-feed; see http://haraka.github.io/barelf/', () => {
1549
+ this.reset_transaction()
1550
+ })
1551
+ return
1552
+ }
1553
+
1554
+ // Stop accumulating data as we're going to reject at dot.
1555
+ if (cfg.max.bytes && this.transaction.data_bytes > cfg.max.bytes) {
1556
+ return
1557
+ }
1558
+
1559
+ if (this.transaction.mime_part_count >= cfg.max.mime_parts) {
1560
+ this.logcrit('Possible DoS attempt - too many MIME parts')
1561
+ this.respond(554, 'Transaction failed due to too many MIME parts', () => {
1562
+ this.disconnect()
1563
+ })
1564
+ return
1565
+ }
1566
+
1567
+ this.transaction.add_data(line)
1568
+ }
1569
+ data_done() {
1570
+ this.pause()
1571
+ this.totalbytes += this.transaction.data_bytes
1572
+
1573
+ // Check message size limit
1574
+ if (cfg.max.bytes && this.transaction.data_bytes > cfg.max.bytes) {
1575
+ this.lognotice(`Incoming message exceeded max size of ${cfg.max.bytes}`)
1576
+ return plugins.run_hooks('max_data_exceeded', this)
1577
+ }
1578
+
1579
+ // Check max received headers count
1580
+ if (this.transaction.header.get_all('received').length > cfg.headers.max_received) {
1581
+ this.logerror('Incoming message had too many Received headers')
1582
+ this.respond(550, 'Too many received headers - possible mail loop', () => {
1583
+ this.reset_transaction()
1584
+ })
1585
+ return
1586
+ }
1587
+
1588
+ // Warn if we hit the maximum parsed header lines limit
1589
+ if (this.transaction.header_lines.length >= cfg.headers.max_lines) {
1590
+ this.logwarn(`Incoming message reached maximum parsing limit of ${cfg.headers.max_lines} header lines`)
1591
+ }
1592
+
1593
+ if (cfg.headers.clean_auth_results) {
1594
+ this.auth_results_clean() // rename old A-R headers
1595
+ }
1596
+ const ar_field = this.auth_results() // assemble new one
1597
+ if (ar_field) {
1598
+ this.transaction.add_header('Authentication-Results', ar_field)
1599
+ }
1600
+
1601
+ this.transaction.end_data(() => {
1602
+ // As this will be called asynchronously,
1603
+ // make sure we still have a transaction.
1604
+ if (!this.transaction) return
1605
+ // Record the start time of this hook as we can't take too long
1606
+ // as the client will typically hang up after 2 to 3 minutes
1607
+ // despite the RFC mandating that 10 minutes should be allowed.
1608
+ this.transaction.data_post_start = Date.now()
1609
+ plugins.run_hooks('data_post', this)
1610
+ })
1611
+ }
1612
+ data_post_respond(retval, msg) {
1613
+ if (!this.transaction) return
1614
+ this.transaction.data_post_delay = (Date.now() - this.transaction.data_post_start) / 1000
1615
+ const mid = this.transaction.header.get('Message-ID') || ''
1616
+ this.lognotice('message', {
1617
+ mid: mid.replace(/\r?\n/, ''),
1618
+ size: this.transaction.data_bytes,
1619
+ rcpts: `${this.transaction.rcpt_count.accept}/${this.transaction.rcpt_count.tempfail}/${this.transaction.rcpt_count.reject}`,
1620
+ delay: this.transaction.data_post_delay,
1621
+ code: constants.translate(retval),
1622
+ msg: msg || '',
1623
+ })
1624
+ const ar_field = this.auth_results() // assemble A-R header
1625
+ if (ar_field) {
1626
+ this.transaction.remove_header('Authentication-Results')
1627
+ this.transaction.add_leading_header('Authentication-Results', ar_field)
1628
+ }
1629
+ switch (retval) {
1630
+ case constants.deny:
1631
+ this.respond(550, msg || 'Message denied', () => {
1632
+ this.msg_count.reject++
1633
+ this.transaction.msg_status = 'rejected'
1634
+ this.reset_transaction(() => this.resume())
1635
+ })
1636
+ break
1637
+ case constants.denydisconnect:
1638
+ this.respond(550, msg || 'Message denied', () => {
1639
+ this.msg_count.reject++
1640
+ this.transaction.msg_status = 'rejected'
1641
+ this.disconnect()
1642
+ })
1643
+ break
1644
+ case constants.denysoft:
1645
+ this.respond(450, msg || 'Message denied temporarily', () => {
1646
+ this.msg_count.tempfail++
1647
+ this.transaction.msg_status = 'deferred'
1648
+ this.reset_transaction(() => this.resume())
1649
+ })
1650
+ break
1651
+ case constants.denysoftdisconnect:
1652
+ this.respond(450, msg || 'Message denied temporarily', () => {
1653
+ this.msg_count.tempfail++
1654
+ this.transaction.msg_status = 'deferred'
1655
+ this.disconnect()
1656
+ })
1657
+ break
1658
+ default:
1659
+ if (this.relaying) {
1660
+ plugins.run_hooks('queue_outbound', this)
1661
+ } else {
1662
+ plugins.run_hooks('queue', this)
1663
+ }
1664
+ }
1665
+ }
1666
+ max_data_exceeded_respond(retval) {
1667
+ // TODO: Maybe figure out what to do with other return codes
1668
+ this.respond(retval === constants.denysoft ? 450 : 550, 'Message too big!', () => {
1669
+ this.reset_transaction()
1670
+ })
1671
+ }
1672
+ queue_msg(retval, msg) {
1673
+ if (msg) {
1674
+ if (typeof msg === 'object' && msg.constructor.name === 'DSN') {
1675
+ return msg.reply
1676
+ }
1677
+ return msg
1678
+ }
1679
+
1680
+ switch (retval) {
1681
+ case constants.ok:
1682
+ return 'Message Queued'
1683
+ case constants.deny:
1684
+ case constants.denydisconnect:
1685
+ return 'Message denied'
1686
+ case constants.denysoft:
1687
+ case constants.denysoftdisconnect:
1688
+ return 'Message denied temporarily'
1689
+ default:
1690
+ return ''
1691
+ }
1692
+ }
1693
+ store_queue_result(retval, msg) {
1694
+ const res_as = { name: 'queue' }
1695
+ switch (retval) {
1696
+ case constants.ok:
1697
+ this.transaction.results.add(res_as, { pass: msg })
1698
+ break
1699
+ case constants.deny:
1700
+ case constants.denydisconnect:
1701
+ this.transaction.results.add(res_as, { fail: msg })
1702
+ break
1703
+ case constants.denysoft:
1704
+ case constants.denysoftdisconnect:
1705
+ this.transaction.results.add(res_as, { fail: msg, soft: true })
1706
+ break
1707
+ case constants.cont:
1708
+ break
1709
+ default:
1710
+ this.transaction.results.add(res_as, { msg })
1711
+ break
1712
+ }
1713
+ }
1714
+ queue_outbound_respond(retval, msg) {
1715
+ if (this.remote.closed) return
1716
+ msg = this.queue_msg(retval, msg) || 'Message Queued'
1717
+ this.store_queue_result(retval, msg)
1718
+ msg = `${msg} (${this.transaction.uuid})`
1719
+ if (retval !== constants.ok) {
1720
+ this.lognotice('queue', {
1721
+ code: constants.translate(retval),
1722
+ msg,
1723
+ })
1724
+ }
1725
+ switch (retval) {
1726
+ case constants.ok:
1727
+ plugins.run_hooks('queue_ok', this, msg)
1728
+ break
1729
+ case constants.deny:
1730
+ this.respond(550, msg, () => {
1731
+ this.msg_count.reject++
1732
+ this.transaction.msg_status = 'rejected'
1733
+ this.reset_transaction(() => this.resume())
1734
+ })
1735
+ break
1736
+ case constants.denydisconnect:
1737
+ this.respond(550, msg, () => {
1738
+ this.msg_count.reject++
1739
+ this.transaction.msg_status = 'rejected'
1740
+ this.disconnect()
1741
+ })
1742
+ break
1743
+ case constants.denysoft:
1744
+ this.respond(450, msg, () => {
1745
+ this.msg_count.tempfail++
1746
+ this.transaction.msg_status = 'deferred'
1747
+ this.reset_transaction(() => this.resume())
1748
+ })
1749
+ break
1750
+ case constants.denysoftdisconnect:
1751
+ this.respond(450, msg, () => {
1752
+ this.msg_count.tempfail++
1753
+ this.transaction.msg_status = 'deferred'
1754
+ this.disconnect()
1755
+ })
1756
+ break
1757
+ default:
1758
+ outbound.send_trans_email(this.transaction, (retval2, msg2) => {
1759
+ if (!msg2) msg2 = this.queue_msg(retval2, msg)
1760
+ switch (retval2) {
1761
+ case constants.ok:
1762
+ if (!msg2) msg2 = this.queue_msg(retval2, msg2)
1763
+ plugins.run_hooks('queue_ok', this, msg2)
1764
+ break
1765
+ case constants.deny:
1766
+ if (!msg2) msg2 = this.queue_msg(retval2, msg2)
1767
+ this.respond(550, msg2, () => {
1768
+ this.msg_count.reject++
1769
+ this.transaction.msg_status = 'rejected'
1770
+ this.reset_transaction(() => {
1771
+ this.resume()
1772
+ })
1773
+ })
1774
+ break
1775
+ default:
1776
+ this.logerror(`Unrecognized response from outbound layer: ${retval2} : ${msg2}`)
1777
+ this.respond(550, msg2 || 'Internal Server Error', () => {
1778
+ this.msg_count.reject++
1779
+ this.transaction.msg_status = 'rejected'
1780
+ this.reset_transaction(() => {
1781
+ this.resume()
1782
+ })
1783
+ })
1784
+ }
1785
+ })
1786
+ }
1787
+ }
1788
+ queue_respond(retval, msg) {
1789
+ msg = this.queue_msg(retval, msg)
1790
+ this.store_queue_result(retval, msg)
1791
+ msg = `${msg} (${this.transaction.uuid})`
1792
+
1793
+ if (retval !== constants.ok) {
1794
+ this.lognotice('queue', {
1795
+ code: constants.translate(retval),
1796
+ msg,
1797
+ })
1798
+ }
1799
+ switch (retval) {
1800
+ case constants.ok:
1801
+ plugins.run_hooks('queue_ok', this, msg)
1802
+ break
1803
+ case constants.deny:
1804
+ this.respond(550, msg, () => {
1805
+ this.msg_count.reject++
1806
+ this.transaction.msg_status = 'rejected'
1807
+ this.reset_transaction(() => this.resume())
1808
+ })
1809
+ break
1810
+ case constants.denydisconnect:
1811
+ this.respond(550, msg, () => {
1812
+ this.msg_count.reject++
1813
+ this.transaction.msg_status = 'rejected'
1814
+ this.disconnect()
1815
+ })
1816
+ break
1817
+ case constants.denysoft:
1818
+ this.respond(450, msg, () => {
1819
+ this.msg_count.tempfail++
1820
+ this.transaction.msg_status = 'deferred'
1821
+ this.reset_transaction(() => this.resume())
1822
+ })
1823
+ break
1824
+ case constants.denysoftdisconnect:
1825
+ this.respond(450, msg, () => {
1826
+ this.msg_count.tempfail++
1827
+ this.transaction.msg_status = 'deferred'
1828
+ this.disconnect()
1829
+ })
1830
+ break
1831
+ default:
1832
+ if (!msg) msg = 'Queuing declined or disabled, try later'
1833
+ this.respond(451, msg, () => {
1834
+ this.msg_count.tempfail++
1835
+ this.transaction.msg_status = 'deferred'
1836
+ this.reset_transaction(() => this.resume())
1837
+ })
1838
+ break
1839
+ }
1840
+ }
1841
+ queue_ok_respond(retval, msg, params) {
1842
+ // This hook is common to both hook_queue and hook_queue_outbound
1843
+ // retval and msg are ignored in this hook so we always log OK
1844
+ this.lognotice('queue', {
1845
+ code: 'OK',
1846
+ msg: params || '',
1847
+ })
1848
+
1849
+ this.respond(250, params, () => {
1850
+ this.msg_count.accept++
1851
+ if (this.transaction) this.transaction.msg_status = 'accepted'
1852
+ this.reset_transaction(() => this.resume())
1853
+ })
1854
+ }
1855
+ }
1856
+
1857
+ exports.Connection = Connection
1858
+
1859
+ exports.cfg = cfg
1860
+
1861
+ exports.createConnection = (client, server, cfg) => {
1862
+ return new Connection(client, server, cfg)
1863
+ }
1864
+
1865
+ logger.add_log_methods(Connection)