• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

haraka / Haraka / 26932611337

04 Jun 2026 05:23AM UTC coverage: 72.387% (+0.1%) from 72.285%
26932611337

push

github

web-flow
Add logging to address validation errors (#3581)

add log lines for address validation errors so that they can be properly debugged.

1670 of 2203 branches covered (75.81%)

6 of 12 new or added lines in 1 file covered. (50.0%)

2 existing lines in 1 file now uncovered.

7631 of 10542 relevant lines covered (72.39%)

25.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.63
/connection.js
1
'use strict'
2✔
2
// a single connection
2✔
3

2✔
4
const dns = require('node:dns')
2✔
5
const os = require('node:os')
2✔
6

2✔
7
// npm libs
2✔
8
const ipaddr = require('ipaddr.js')
2✔
9
const config = require('haraka-config')
2✔
10
const constants = require('haraka-constants')
2✔
11
const net_utils = require('haraka-net-utils')
2✔
12
const Notes = require('haraka-notes')
2✔
13
const utils = require('haraka-utils')
2✔
14
const { Address } = require('./address')
2✔
15
const ResultStore = require('haraka-results')
2✔
16

2✔
17
// Haraka libs
2✔
18
const logger = require('./logger')
2✔
19
const trans = require('./transaction')
2✔
20
const plugins = require('./plugins')
2✔
21
const rfc1869 = utils.rfc1869
2✔
22
const outbound = require('./outbound')
2✔
23

2✔
24
const states = constants.connection.state
2✔
25

2✔
26
const cfg = config.get('connection.ini', {
2✔
27
    booleans: [
2✔
28
        '-main.strict_rfc1869',
2✔
29
        '+main.smtputf8',
2✔
30
        '+headers.add_received',
2✔
31
        '+headers.show_version',
2✔
32
        '+headers.clean_auth_results',
2✔
33
    ],
2✔
34
})
2✔
35

2✔
36
class Connection {
2✔
37
    constructor(client, server) {
2✔
38
        this.client = client
73✔
39
        this.server = server
73✔
40

73✔
41
        this.local = {
73✔
42
            ip: null,
73✔
43
            port: null,
73✔
44
            host: net_utils.get_primary_host_name(),
73✔
45
            info: 'Haraka',
73✔
46
        }
73✔
47
        this.remote = {
73✔
48
            ip: null,
73✔
49
            port: null,
73✔
50
            host: null,
73✔
51
            info: null,
73✔
52
            closed: false,
73✔
53
            is_private: false,
73✔
54
            is_local: false,
73✔
55
        }
73✔
56
        this.hello = {
73✔
57
            host: null,
73✔
58
            verb: null,
73✔
59
        }
73✔
60
        this.tls = {
73✔
61
            enabled: false,
73✔
62
            advertised: false,
73✔
63
            verified: false,
73✔
64
            cipher: {},
73✔
65
        }
73✔
66
        this.proxy = {
73✔
67
            allowed: false,
73✔
68
            ip: null,
73✔
69
            type: null,
73✔
70
            timer: null,
73✔
71
        }
73✔
72
        this.set('tls', 'enabled', !!server.has_tls)
73✔
73

73✔
74
        this.current_data = null
73✔
75
        this.current_line = null
73✔
76
        this.state = states.PAUSE
73✔
77
        this.encoding = 'utf8'
73✔
78
        this.prev_state = null
73✔
79
        this.loop_code = null
73✔
80
        this.loop_msg = null
73✔
81
        this.uuid = utils.uuid()
73✔
82
        this.notes = new Notes()
73✔
83
        this.transaction = null
73✔
84
        this.tran_count = 0
73✔
85
        this.capabilities = null
73✔
86
        this.early_talker = false
73✔
87
        this.pipelining = false
73✔
88
        this._relaying = false
73✔
89
        this.esmtp = false
73✔
90
        this.last_response = null
73✔
91
        this.hooks_to_run = []
73✔
92
        this.start_time = Date.now()
73✔
93
        this.last_reject = ''
73✔
94
        this.totalbytes = 0
73✔
95
        this.rcpt_count = {
73✔
96
            accept: 0,
73✔
97
            tempfail: 0,
73✔
98
            reject: 0,
73✔
99
        }
73✔
100
        this.msg_count = {
73✔
101
            accept: 0,
73✔
102
            tempfail: 0,
73✔
103
            reject: 0,
73✔
104
        }
73✔
105
        this.results = new ResultStore(this)
73✔
106
        this.errors = 0
73✔
107
        this.last_rcpt_msg = null
73✔
108
        this.hook = null
73✔
109
        if (cfg.headers.show_version) {
73✔
110
            this.local.info += `/${utils.getVersion(__dirname)}`
73✔
111
        }
73✔
112
        Connection.setupClient(this)
73✔
113
    }
73✔
114
    static setupClient(self) {
2✔
115
        const ip = self.client.remoteAddress
73✔
116
        if (!ip) {
73✔
117
            self.logdebug('setupClient got no IP address for this connection!')
59✔
118
            self.client.destroy()
59✔
119
            return
59✔
120
        }
59✔
121

5✔
122
        const local_addr = self.server.address()
5✔
123
        self.set('local', 'ip', ipaddr.process(self.client.localAddress || local_addr.address).toString())
73!
124
        self.set('local', 'port', self.client.localPort || local_addr.port)
73!
125
        self.results.add({ name: 'local' }, self.local)
73✔
126

73✔
127
        self.set('remote', 'ip', ipaddr.process(ip).toString())
73✔
128
        self.set('remote', 'port', self.client.remotePort)
73✔
129
        self.results.add({ name: 'remote' }, self.remote)
73✔
130

73✔
131
        self.lognotice('connect', {
73✔
132
            ip: self.remote.ip,
73✔
133
            port: self.remote.port,
73✔
134
            local_ip: self.local.ip,
73✔
135
            local_port: self.local.port,
73✔
136
        })
73✔
137

73✔
138
        if (!self.client.on) return
73✔
139

73✔
140
        const log_data = { ip: self.remote.ip }
73✔
141
        if (self.remote.host) log_data.host = self.remote.host
73!
142

73✔
143
        self.client.on('end', () => {
73✔
144
            if (self.state >= states.DISCONNECTING) return
8✔
145
            self.remote.closed = true
4✔
146
            self.loginfo('client half closed connection', log_data)
4✔
147
            self.fail()
4✔
148
        })
73✔
149

73✔
150
        self.client.on('close', () => {
73✔
151
            if (self.state >= states.DISCONNECTING) return
9!
152
            self.remote.closed = true
×
153
            self.loginfo('client dropped connection', log_data)
×
154
            self.fail()
×
155
        })
73✔
156

73✔
157
        self.client.on('error', (err) => {
73✔
158
            if (self.state >= states.DISCONNECTING) return
2✔
159
            self.loginfo(`client connection error: ${err}`, log_data)
1✔
160
            self.fail()
1✔
161
        })
73✔
162

73✔
163
        self.client.on('timeout', () => {
73✔
164
            // FIN has sent, when timeout just destroy socket
×
165
            if (self.state >= states.DISCONNECTED) {
×
166
                self.client.destroy()
×
167
                self.loginfo(`timeout, destroy socket (state:${self.state})`)
×
168
                return
×
169
            }
×
170
            if (self.state >= states.DISCONNECTING) return
×
171
            self.respond(421, 'timeout', () => {
×
172
                self.fail('client connection timed out', log_data)
×
173
            })
×
174
        })
73✔
175

73✔
176
        self.client.on('data', (data) => {
73✔
177
            self.process_data(data)
29✔
178
        })
73✔
179

73✔
180
        // SMTPS pre-parser state: proxy means the PROXY line was already consumed;
73✔
181
        // peer_allowed means a trusted PROXY peer sent direct TLS instead.
73✔
182
        const smtps = self.client.haraka_smtps
73✔
183
        if (smtps?.proxy) {
73✔
184
            self.proxy.allowed = true
1✔
185
            return
1✔
186
        }
1✔
187

8✔
188
        if (smtps?.peer_allowed) {
73✔
189
            plugins.run_hooks('connect_init', self)
2✔
190
            return
2✔
191
        }
2✔
192

6✔
193
        if (net_utils.is_haproxy_allowed(self.remote.ip)) {
73!
194
            self.proxy.allowed = true
×
195
            // Wait for PROXY command
×
196
            self.proxy.timer = setTimeout(() => {
×
197
                self.respond(421, 'PROXY timeout', () => {
×
198
                    self.disconnect()
×
199
                })
×
200
            }, 30 * 1000)
×
201
        } else {
73✔
202
            plugins.run_hooks('connect_init', self)
6✔
203
        }
6✔
204
    }
73✔
205
    setTLS(obj) {
2✔
206
        this.set('hello', 'host', undefined)
4✔
207
        this.set('tls', 'enabled', true)
4✔
208
        for (const t of ['cipher', 'verified', 'verifyError', 'peerCertificate']) {
4✔
209
            if (obj[t] === undefined) continue
16!
210
            this.set('tls', t, obj[t])
16✔
211
        }
16✔
212
        // prior to 2017-07, authorized and verified were both used. Verified
4✔
213
        // seems to be the more common and has the property updated in the
4✔
214
        // tls object. However, authorized has been up-to-date in the notes. Store
4✔
215
        // in both, for backwards compatibility.
4✔
216
        this.notes.tls = {
4✔
217
            authorized: obj.verified, // legacy name
4✔
218
            authorizationError: obj.verifyError,
4✔
219
            cipher: obj.cipher,
4✔
220
            peerCertificate: obj.peerCertificate,
4✔
221
        }
4✔
222
    }
4✔
223
    set(prop_str, val) {
2✔
224
        if (arguments.length === 3) {
243✔
225
            prop_str = `${arguments[0]}.${arguments[1]}`
188✔
226
            val = arguments[2]
188✔
227
        }
188✔
228

243✔
229
        const path_parts = prop_str.split('.')
243✔
230
        let loc = this
243✔
231
        for (let i = 0; i < path_parts.length; i++) {
243✔
232
            const part = path_parts[i]
484✔
233
            if (part === '__proto__' || part === 'constructor') continue
484!
234

484✔
235
            // while another part remains
484✔
236
            if (i < path_parts.length - 1) {
484✔
237
                if (loc[part] === undefined) loc[part] = {} // initialize
241✔
238
                loc = loc[part] // descend
241✔
239
                continue
241✔
240
            }
241✔
241

243✔
242
            // last part, so assign the value
243✔
243
            loc[part] = val
243✔
244
        }
243✔
245

243✔
246
        // Set is_private, is_local automatically when remote.ip is set
243✔
247
        if (prop_str === 'remote.ip') {
243✔
248
            this.set('remote.is_local', net_utils.is_local_ip(this.remote.ip))
17✔
249
            if (this.remote.is_local) {
17✔
250
                this.set('remote.is_private', true)
2✔
251
            } else {
17✔
252
                this.set('remote.is_private', net_utils.is_private_ip(this.remote.ip))
5✔
253
            }
5✔
254
        }
17✔
255
    }
243✔
256
    get(prop_str) {
2✔
257
        return prop_str.split('.').reduce((prev, curr) => {
9✔
258
            return prev ? prev[curr] : undefined
18!
259
        }, this)
9✔
260
    }
9✔
261
    set relaying(val) {
2✔
262
        if (this.transaction) {
6✔
263
            this.transaction._relaying = val
1✔
264
        } else {
6✔
265
            this._relaying = val
3✔
266
        }
3✔
267
    }
6✔
268
    get relaying() {
2✔
269
        if (this.transaction && '_relaying' in this.transaction) return this.transaction._relaying
23✔
270
        return this._relaying
4✔
271
    }
23✔
272
    process_line(line) {
2✔
273
        if (this.state >= states.DISCONNECTING) {
39!
274
            if (logger.would_log(logger.LOGPROTOCOL)) {
×
275
                this.logprotocol(`C: (after-disconnect): ${this.current_line}`, {
×
276
                    state: this.state,
×
277
                })
×
278
            }
×
279
            this.loginfo(`data after disconnect from ${this.remote.ip}`)
×
280
            return
×
281
        }
×
282

39✔
283
        if (this.state === states.DATA) {
39✔
284
            if (logger.would_log(logger.LOGDATA)) {
14✔
285
                this.logdata(`C: ${line}`)
14✔
286
            }
14✔
287
            this.accumulate_data(line)
14✔
288
            return
14✔
289
        }
14✔
290

23✔
291
        this.current_line = line.toString(this.encoding).replace(/\r?\n/, '')
23✔
292
        if (logger.would_log(logger.LOGPROTOCOL)) {
39!
293
            this.logprotocol(`C: ${this.current_line}`, { state: this.state })
×
294
        }
✔
295

23✔
296
        // Check for non-ASCII characters
23✔
297
        /* eslint no-control-regex: 0 */
23✔
298
        if (/[^\x00-\x7F]/.test(this.current_line)) {
39!
299
            // See if this is a TLS handshake
×
300
            const buf = Buffer.from(this.current_line.slice(0, 3), 'binary')
×
301
            if (
×
302
                buf[0] === 0x16 &&
×
303
                buf[1] === 0x03 &&
×
304
                (buf[2] === 0x00 || buf[2] === 0x01) // SSLv3/TLS1.x format
×
305
            ) {
×
306
                // Nuke the current input buffer to prevent processing further input
×
307
                this.current_data = null
×
308
                this.respond(501, 'SSL attempted over a non-SSL socket')
×
309
                this.disconnect()
×
310
                return
×
311
            } else if (this.hello.verb == 'HELO') {
×
312
                return this.respond(501, 'Syntax error (8-bit characters not allowed)')
×
313
            }
×
314
        }
✔
315

23✔
316
        if (this.state === states.CMD) {
23✔
317
            this.state = states.PAUSE_SMTP
23✔
318
            const matches = /^([^ ]*)( +(.*))?$/.exec(this.current_line)
23✔
319
            if (!matches) {
23!
320
                return plugins.run_hooks('unrecognized_command', this, [this.current_line])
×
321
            }
×
322
            const cmd = matches[1]
23✔
323
            const method = `cmd_${cmd.toLowerCase()}`
23✔
324
            const remaining = matches[3] || ''
23✔
325
            if (this[method]) {
23✔
326
                try {
18✔
327
                    this[method](remaining)
18✔
328
                } catch (err) {
18!
329
                    if (err.stack) {
×
330
                        this.logerror(`${method} failed: ${err}`)
×
331
                        for (const line of err.stack.split('\n')) this.logerror(line)
×
332
                    } else {
×
333
                        this.logerror(`${method} failed: ${err}`)
×
334
                    }
×
335
                    this.respond(421, 'Internal Server Error', () => {
×
336
                        this.disconnect()
×
337
                    })
×
338
                }
×
339
            } else {
23✔
340
                // unrecognized command
5✔
341
                plugins.run_hooks('unrecognized_command', this, [cmd, remaining])
5✔
342
            }
5✔
343
        } else if (this.state === states.LOOP) {
39✔
344
            // Allow QUIT
1✔
345
            if (this.current_line.toUpperCase() === 'QUIT') {
1✔
346
                this.state = states.PAUSE_SMTP
1✔
347
                this.cmd_quit()
1✔
348
            } else {
1!
349
                this.respond(this.loop_code, this.loop_msg)
×
350
            }
×
351
        } else if (this.state === states.PAUSE_SMTP) {
39✔
352
            // Do nothing
1✔
353
        } else {
1!
354
            throw new Error(`unknown state ${this.state}`)
×
355
        }
×
356
    }
39✔
357
    process_data(data) {
2✔
358
        if (this.state >= states.DISCONNECTING) {
29!
359
            this.loginfo(`data after disconnect from ${this.remote.ip}`)
×
360
            return
×
361
        }
×
362

29✔
363
        if (!this.current_data || !this.current_data.length) {
29✔
364
            this.current_data = data
29✔
365
        } else {
29!
366
            // Data left over in buffer
×
367
            const buf = Buffer.concat([this.current_data, data], this.current_data.length + data.length)
×
368
            this.current_data = buf
×
369
        }
×
370

29✔
371
        this._process_data()
29✔
372
    }
29✔
373
    _process_data() {
2✔
374
        // We *must* detect disconnected connections here as the state
76✔
375
        // only transitions to states.CMD in the respond function below.
76✔
376
        // Otherwise if multiple commands are pipelined and then the
76✔
377
        // connection is dropped; we'll end up in the function forever.
76✔
378
        if (this.state >= states.DISCONNECTING) return
76✔
379

66✔
380
        let maxlength
66✔
381
        if (this.state === states.PAUSE_DATA || this.state === states.DATA) {
76✔
382
            maxlength = cfg.max.data_line_length
9✔
383
        } else {
76✔
384
            maxlength = cfg.max.line_length
57✔
385
        }
57✔
386

66✔
387
        let offset
66✔
388
        while (this.current_data && (offset = utils.indexOfLF(this.current_data, maxlength)) !== -1) {
76✔
389
            if (this.state === states.PAUSE_DATA) {
37✔
390
                return
37✔
391
            }
37✔
392
            let this_line = this.current_data.slice(0, offset + 1)
37✔
393
            // Hack: bypass this code to allow HAProxy's PROXY extension
37✔
394
            const proxyStart = this.proxy.allowed && /^PROXY /.test(this_line)
37✔
395
            if (this.state === states.PAUSE && proxyStart) {
37✔
396
                if (this.proxy.timer) clearTimeout(this.proxy.timer)
37✔
397
                this.state = states.CMD
37✔
398
                this.current_data = this.current_data.slice(this_line.length)
37✔
399
                this.process_line(this_line)
37✔
400
            }
37✔
401
            // Detect early_talker but allow PIPELINING extension (ESMTP)
37✔
402
            else if ((this.state === states.PAUSE || this.state === states.PAUSE_SMTP) && !this.esmtp) {
37✔
403
                // Allow EHLO/HELO to be pipelined with PROXY
37✔
404
                if (this.proxy.allowed && /^(?:EH|HE)LO /i.test(this_line)) return
37✔
405
                if (!this.early_talker) {
37✔
406
                    this_line = this_line.toString().replace(/\r?\n/, '')
37✔
407
                    this.logdebug('[early_talker]', {
37✔
408
                        state: this.state,
37✔
409
                        esmtp: this.esmtp,
37✔
410
                        line: this_line,
37✔
411
                    })
37✔
412
                }
37✔
413
                this.early_talker = true
37✔
414
                setImmediate(() => {
37✔
415
                    this._process_data()
×
416
                })
37✔
417
                break
37✔
418
            } else if ((this.state === states.PAUSE || this.state === states.PAUSE_SMTP) && this.esmtp) {
37✔
419
                let valid = true
37✔
420
                const cmd = this_line.toString('ascii').slice(0, 4).toUpperCase()
37✔
421
                switch (cmd) {
37✔
422
                    case 'RSET':
37✔
423
                    case 'MAIL':
37✔
424
                    case 'SEND':
37✔
425
                    case 'SOML':
37✔
426
                    case 'SAML':
37✔
427
                    case 'RCPT':
37✔
428
                        // These can be anywhere in the group
37✔
429
                        break
37✔
430
                    default:
37✔
431
                        // Anything else *MUST* be the last command in the group
37✔
432
                        if (this_line.length !== this.current_data.length) {
37✔
433
                            valid = false
37✔
434
                        }
37✔
435
                        break
37✔
436
                }
37✔
437
                if (valid) {
37✔
438
                    // Valid PIPELINING
37✔
439
                    // We *don't want to process this yet otherwise the
37✔
440
                    // current_data buffer will be lost.  The respond()
37✔
441
                    // function will call this function again once it
37✔
442
                    // has reset the state back to states.CMD and this
37✔
443
                    // ensures that we only process one command at a
37✔
444
                    // time.
37✔
445
                    this.pipelining = true
37✔
446
                    this.logdebug(`pipeline: ${this_line}`)
37✔
447
                } else {
37✔
448
                    // Invalid pipeline sequence
37✔
449
                    // Treat this as early talker
37✔
450
                    if (!this.early_talker) {
37✔
451
                        this.logdebug('[early_talker]', {
37✔
452
                            state: this.state,
37✔
453
                            esmtp: this.esmtp,
37✔
454
                            line: this_line,
37✔
455
                        })
37✔
456
                    }
37✔
457
                    this.early_talker = true
37✔
458
                    setImmediate(() => {
37✔
459
                        this._process_data()
×
460
                    })
37✔
461
                }
37✔
462
                break
37✔
463
            } else {
37✔
464
                this.current_data = this.current_data.slice(this_line.length)
37✔
465
                this.process_line(this_line)
37✔
466
            }
37✔
467
        }
37✔
468

66✔
469
        if (
66✔
470
            this.current_data &&
76✔
471
            this.current_data.length > maxlength &&
76!
472
            utils.indexOfLF(this.current_data, maxlength) === -1
×
473
        ) {
76!
474
            if (this.state !== states.DATA && this.state !== states.PAUSE_DATA) {
×
475
                // In command mode, reject:
×
476
                this.client.pause()
×
477
                this.current_data = null
×
478
                return this.respond(521, 'Command line too long', () => {
×
479
                    this.disconnect()
×
480
                })
×
481
            } else {
×
482
                this.loginfo(`DATA line length (${this.current_data.length}) exceeds limit of ${maxlength} bytes`)
×
483
                this.transaction.notes.data_line_length_exceeded = true
×
484
                const b = Buffer.concat(
×
485
                    [
×
486
                        this.current_data.slice(0, maxlength - 2),
×
487
                        Buffer.from('\r\n ', 'utf8'),
×
488
                        this.current_data.slice(maxlength - 2),
×
489
                    ],
×
490
                    this.current_data.length + 3,
×
491
                )
×
492
                this.current_data = b
×
493
                return this._process_data()
×
494
            }
×
495
        }
×
496
    }
76✔
497
    respond(code, msg, func) {
2✔
498
        let uuid = ''
50✔
499
        let messages
50✔
500

50✔
501
        if (this.state === states.DISCONNECTED) {
50✔
502
            if (func) func()
2✔
503
            return
2✔
504
        }
2✔
505
        // Check to see if DSN object was passed in
13✔
506
        if (typeof msg === 'object' && msg.constructor.name === 'DSN') {
50✔
507
            // Override
3✔
508
            code = msg.code
3✔
509
            msg = msg.reply
3✔
510
        }
3✔
511

13✔
512
        if (!Array.isArray(msg)) {
50✔
513
            messages = msg.toString().split(/\n/)
42✔
514
        } else {
50✔
515
            messages = msg.slice()
6✔
516
        }
6✔
517
        messages = messages.filter((msg2) => /\S/.test(msg2))
13✔
518

13✔
519
        // Multiline AUTH PLAIN as in RFC-4954 page 8.
13✔
520
        if (code === 334 && !messages.length) {
50!
521
            messages = [' ']
×
522
        }
✔
523

13✔
524
        if (code >= 400) {
50✔
525
            this.last_reject = `${code} ${messages.join(' ')}`
8✔
526
            if (cfg.uuid.deny_chars) {
8!
527
                uuid = (this.transaction || this).uuid
×
528
                if (cfg.uuid.deny_chars > 1) {
×
529
                    uuid = uuid.slice(0, cfg.uuid.deny_chars)
×
530
                }
×
531
            }
×
532
        }
8✔
533

13✔
534
        let mess
13✔
535
        let buf = ''
13✔
536
        const hostname = os.hostname().split('.').shift()
13✔
537
        const _uuid = uuid ? `[${uuid}@${hostname}] ` : ''
50!
538

50✔
539
        while ((mess = messages.shift())) {
50✔
540
            const line = `${code}${messages.length ? '-' : ' '}${_uuid}${mess}`
65✔
541
            this.logprotocol(`S: ${line}`)
65✔
542
            buf = `${buf}${line}\r\n`
65✔
543
        }
65✔
544

13✔
545
        if (this.client.write === undefined) return buf // testing
50✔
546

1✔
547
        try {
1✔
548
            this.client.write(buf)
1✔
549
        } catch (err) {
50!
550
            return this.fail(`Writing response: ${buf} failed: ${err}`)
×
551
        }
✔
552

1✔
553
        // Store the last response
1✔
554
        this.last_response = buf
1✔
555

1✔
556
        // Don't change loop state
1✔
557
        if (this.state !== states.LOOP) {
1✔
558
            this.state = states.CMD
1✔
559
        }
1✔
560

1✔
561
        // Run optional closure before handling and further commands
1✔
562
        if (func) func()
50✔
563

1✔
564
        // Process any buffered commands (PIPELINING)
1✔
565
        this._process_data()
1✔
566
    }
50✔
567
    fail(err, err_data) {
2✔
568
        if (err) this.logwarn(err, err_data)
5!
569
        this.hooks_to_run = []
5✔
570
        this.disconnect()
5✔
571
    }
5✔
572
    disconnect() {
2✔
573
        if (this.state >= states.DISCONNECTING) return
9!
574
        this.state = states.DISCONNECTING
9✔
575
        this.current_data = null // don't process any more data we have already received
9✔
576
        this.reset_transaction(() => {
9✔
577
            plugins.run_hooks('disconnect', this)
9✔
578
        })
9✔
579
    }
9✔
580
    disconnect_respond() {
2✔
581
        const logdetail = {
9✔
582
            ip: this.remote.ip,
9✔
583
            rdns: this.remote.host ? this.remote.host : '',
9!
584
            helo: this.hello.host ? this.hello.host : '',
9✔
585
            relay: this.relaying ? 'Y' : 'N',
9✔
586
            early: this.early_talker ? 'Y' : 'N',
9!
587
            esmtp: this.esmtp ? 'Y' : 'N',
9✔
588
            tls: this.tls.enabled ? 'Y' : 'N',
9✔
589
            pipe: this.pipelining ? 'Y' : 'N',
9!
590
            errors: this.errors,
9✔
591
            txns: this.tran_count,
9✔
592
            rcpts: `${this.rcpt_count.accept}/${this.rcpt_count.tempfail}/${this.rcpt_count.reject}`,
9✔
593
            msgs: `${this.msg_count.accept}/${this.msg_count.tempfail}/${this.msg_count.reject}`,
9✔
594
            bytes: this.totalbytes,
9✔
595
            lr: this.last_reject ? this.last_reject : '',
9✔
596
            time: (Date.now() - this.start_time) / 1000,
9✔
597
        }
9✔
598

9✔
599
        this.results.add(
9✔
600
            { name: 'disconnect' },
9✔
601
            {
9✔
602
                duration: (Date.now() - this.start_time) / 1000,
9✔
603
            },
9✔
604
        )
9✔
605
        this.lognotice('disconnect', logdetail)
9✔
606
        this.state = states.DISCONNECTED
9✔
607
        this.client.end()
9✔
608
    }
9✔
609
    get_capabilities() {
2✔
610
        return []
1✔
611
    }
1✔
612
    tran_uuid() {
2✔
613
        this.tran_count++
6✔
614
        return `${this.uuid}.${this.tran_count}`
6✔
615
    }
6✔
616
    reset_transaction(cb) {
2✔
617
        this.results.add(
22✔
618
            { name: 'reset' },
22✔
619
            {
22✔
620
                duration: (Date.now() - this.start_time) / 1000,
22✔
621
            },
22✔
622
        )
22✔
623
        if (this.transaction && this.transaction.resetting === false) {
22✔
624
            // Pause connection to allow the hook to complete
3✔
625
            this.pause()
3✔
626
            this.transaction.resetting = true
3✔
627
            plugins.run_hooks('reset_transaction', this, cb)
3✔
628
        } else {
22✔
629
            this.transaction = null
19✔
630
            if (cb) cb()
19✔
631
        }
19✔
632
    }
22✔
633
    reset_transaction_respond(retval, msg, cb) {
2✔
634
        if (this.transaction) {
3✔
635
            this.transaction.message_stream.destroy()
3✔
636
            this.transaction = null
3✔
637
        }
3✔
638
        if (cb) cb()
3✔
639
        // Allow the connection to continue
3✔
640
        this.resume()
3✔
641
    }
3✔
642
    init_transaction(cb) {
2✔
643
        this.reset_transaction(() => {
3✔
644
            this.transaction = trans.createTransaction(this.tran_uuid(), cfg)
3✔
645
            // Catch any errors from the message_stream
3✔
646
            this.transaction.message_stream.on('error', (err) => {
3✔
647
                this.logcrit(`message_stream error: ${err.message}`)
×
648
                this.respond('421', 'Internal Server Error', () => {
×
649
                    this.disconnect()
×
650
                })
×
651
            })
3✔
652
            this.transaction.results = new ResultStore(this)
3✔
653
            if (cb) cb()
3✔
654
        })
3✔
655
    }
3✔
656
    loop_respond(code, msg) {
2✔
657
        if (this.state >= states.DISCONNECTING) return
4✔
658
        this.state = states.LOOP
3✔
659
        this.loop_code = code
3✔
660
        this.loop_msg = msg
3✔
661
        this.respond(code, msg)
3✔
662
    }
4✔
663
    pause() {
2✔
664
        if (this.state >= states.DISCONNECTING) return
8!
665
        this.client.pause()
8✔
666
        if (this.state !== states.PAUSE_DATA) this.prev_state = this.state
8✔
667
        this.state = states.PAUSE_DATA
8✔
668
    }
8✔
669
    resume() {
2✔
670
        if (this.state >= states.DISCONNECTING) return
11!
671
        this.client.resume()
11✔
672
        if (this.prev_state && this.state === states.PAUSE_DATA) {
11✔
673
            this.state = this.prev_state
4✔
674
        }
4✔
675
        this.prev_state = null
11✔
676
        setImmediate(() => this._process_data())
11✔
677
    }
11✔
678
    /////////////////////////////////////////////////////////////////////////////
2✔
679
    // SMTP Responses
2✔
680
    connect_init_respond() {
2✔
681
        this.logdebug('running connect_init_respond')
9✔
682
        plugins.run_hooks('lookup_rdns', this)
9✔
683
    }
9✔
684
    lookup_rdns_respond(retval, msg) {
2✔
685
        switch (retval) {
9✔
686
            case constants.ok:
9!
687
                this.set('remote', 'host', msg || 'Unknown')
×
688
                this.set('remote', 'info', this.remote.info || this.remote.host)
×
689
                plugins.run_hooks('connect', this)
×
690
                break
×
691
            case constants.deny:
9!
692
                this.loop_respond(554, msg || 'rDNS Lookup Failed')
×
693
                break
×
694
            case constants.denydisconnect:
9!
695
            case constants.disconnect:
9!
696
                this.respond(554, msg || 'rDNS Lookup Failed', () => {
×
697
                    this.disconnect()
×
698
                })
×
699
                break
×
700
            case constants.denysoft:
9!
701
                this.loop_respond(421, msg || 'rDNS Temporary Failure')
×
702
                break
×
703
            case constants.denysoftdisconnect:
9!
704
                this.respond(421, msg || 'rDNS Temporary Failure', () => {
×
705
                    this.disconnect()
×
706
                })
×
707
                break
×
708
            default:
9✔
709
                // BUG: dns.reverse throws on invalid input (and sometimes valid
9✔
710
                // input nodejs/node#47847). Also throws when empty results
9✔
711
                try {
9✔
712
                    dns.reverse(this.remote.ip, (err, domains) => {
9✔
713
                        this.rdns_response(err, domains)
9✔
714
                    })
9✔
715
                } catch (err) {
9!
716
                    this.rdns_response(err, [])
×
717
                }
×
718
        }
9✔
719
    }
9✔
720
    rdns_response(err, domains) {
2✔
721
        if (err) {
9!
722
            switch (err.code) {
×
723
                case dns.NXDOMAIN:
×
724
                case dns.NOTFOUND:
×
725
                    this.set('remote', 'host', 'NXDOMAIN')
×
726
                    break
×
727
                default:
×
728
                    this.set('remote', 'host', 'DNSERROR')
×
729
                    break
×
730
            }
×
731
        } else {
9✔
732
            this.set('remote', 'host', domains[0] || 'Unknown')
9!
733
            this.results.add({ name: 'remote' }, this.remote)
9✔
734
        }
9✔
735
        this.set('remote', 'info', this.remote.info || this.remote.host)
9✔
736
        plugins.run_hooks('connect', this)
9✔
737
    }
9✔
738
    unrecognized_command_respond(retval, msg) {
2✔
739
        switch (retval) {
4✔
740
            case constants.ok:
4✔
741
                // response already sent, cool...
4✔
742
                break
4✔
743
            case constants.next_hook:
4!
744
                plugins.run_hooks(msg, this)
×
745
                break
×
746
            case constants.deny:
4!
747
                this.respond(500, msg || 'Unrecognized command')
×
748
                break
×
749
            case constants.denydisconnect:
4!
750
            case constants.denysoftdisconnect:
4!
751
                this.respond(retval === constants.denydisconnect ? 521 : 421, msg || 'Unrecognized command', () => {
×
752
                    this.disconnect()
×
753
                })
×
754
                break
×
755
            default:
4!
756
                this.errors++
×
757
                this.respond(500, msg || 'Unrecognized command')
×
758
        }
4✔
759
    }
4✔
760
    connect_respond(retval, msg) {
2✔
761
        // RFC 5321 Section 4.3.2 states that the only valid SMTP codes here are:
9✔
762
        // 220 = Service ready
9✔
763
        // 554 = Transaction failed (no SMTP service here)
9✔
764
        // 421 = Service shutting down and closing transmission channel
9✔
765
        switch (retval) {
9✔
766
            case constants.deny:
9!
767
                this.loop_respond(554, msg || 'Your mail is not welcome here')
×
768
                break
×
769
            case constants.denydisconnect:
9!
770
            case constants.disconnect:
9!
771
                this.respond(554, msg || 'Your mail is not welcome here', () => {
×
772
                    this.disconnect()
×
773
                })
×
774
                break
×
775
            case constants.denysoft:
9!
776
                this.loop_respond(421, msg || 'Come back later')
×
777
                break
×
778
            case constants.denysoftdisconnect:
9!
779
                this.respond(421, msg || 'Come back later', () => {
×
780
                    this.disconnect()
×
781
                })
×
782
                break
×
783
            default: {
9✔
784
                let greeting
9✔
785
                if (cfg.message.greeting?.length) {
9!
786
                    // RFC5321 section 4.2
×
787
                    // Hostname/domain should appear after the 220
×
788
                    greeting = [...cfg.message.greeting]
×
789
                    greeting[0] = `${this.local.host} ESMTP ${greeting[0]}`
×
790
                    if (cfg.uuid.banner_chars) {
×
791
                        greeting[0] += ` (${this.uuid.slice(0, cfg.uuid.banner_chars)})`
×
792
                    }
×
793
                } else {
9✔
794
                    greeting = `${this.local.host} ESMTP ${this.local.info} ready`
9✔
795
                    if (cfg.uuid.banner_chars) {
9✔
796
                        greeting += ` (${this.uuid.slice(0, cfg.uuid.banner_chars)})`
9✔
797
                    }
9✔
798
                }
9✔
799
                this.respond(220, msg || greeting)
9✔
800
            }
9✔
801
        }
9✔
802
    }
9✔
803
    get_remote(prop) {
2✔
804
        switch (this.remote[prop]) {
12✔
805
            case 'NXDOMAIN':
12✔
806
            case 'DNSERROR':
12✔
807
            case '':
12✔
808
            case undefined:
12✔
809
            case null:
12✔
810
                return `[${this.remote.ip}]`
3✔
811
            default:
12✔
812
                return `${this.remote[prop]} [${this.remote.ip}]`
1✔
813
        }
12✔
814
    }
12✔
815
    helo_respond(retval, msg) {
2✔
816
        switch (retval) {
×
817
            case constants.deny:
×
818
                this.respond(550, msg || 'HELO denied', () => {
×
819
                    this.set('hello', 'verb', null)
×
820
                    this.set('hello', 'host', null)
×
821
                })
×
822
                break
×
823
            case constants.denydisconnect:
×
824
                this.respond(550, msg || 'HELO denied', () => {
×
825
                    this.disconnect()
×
826
                })
×
827
                break
×
828
            case constants.denysoft:
×
829
                this.respond(450, msg || 'HELO denied', () => {
×
830
                    this.set('hello', 'verb', null)
×
831
                    this.set('hello', 'host', null)
×
832
                })
×
833
                break
×
834
            case constants.denysoftdisconnect:
×
835
                this.respond(450, msg || 'HELO denied', () => {
×
836
                    this.disconnect()
×
837
                })
×
838
                break
×
839
            default:
×
840
                // RFC5321 section 4.1.1.1
×
841
                // Hostname/domain should appear after 250
×
842
                this.respond(250, `${this.local.host} Hello ${this.get_remote('host')}, ${cfg.message.helo}`)
×
843
        }
×
844
    }
×
845
    ehlo_respond(retval, msg) {
2✔
846
        switch (retval) {
5✔
847
            case constants.deny:
5!
848
                this.respond(550, msg || 'EHLO denied', () => {
×
849
                    this.set('hello', 'verb', null)
×
850
                    this.set('hello', 'host', null)
×
851
                })
×
852
                break
×
853
            case constants.denydisconnect:
5!
854
                this.respond(550, msg || 'EHLO denied', () => {
×
855
                    this.disconnect()
×
856
                })
×
857
                break
×
858
            case constants.denysoft:
5!
859
                this.respond(450, msg || 'EHLO denied', () => {
×
860
                    this.set('hello', 'verb', null)
×
861
                    this.set('hello', 'host', null)
×
862
                })
×
863
                break
×
864
            case constants.denysoftdisconnect:
5!
865
                this.respond(450, msg || 'EHLO denied', () => {
×
866
                    this.disconnect()
×
867
                })
×
868
                break
×
869
            default: {
5✔
870
                // RFC5321 section 4.1.1.1
5✔
871
                // Hostname/domain should appear after 250
5✔
872

5✔
873
                const response = [
5✔
874
                    `${this.local.host} Hello ${this.get_remote('host')}, ${cfg.message.helo}`,
5✔
875
                    'PIPELINING',
5✔
876
                    '8BITMIME',
5✔
877
                ]
5✔
878

5✔
879
                if (cfg.main.smtputf8) response.push('SMTPUTF8')
5✔
880

5✔
881
                response.push(`SIZE ${cfg.max.bytes}`)
5✔
882

5✔
883
                this.capabilities = response
5✔
884

5✔
885
                plugins.run_hooks('capabilities', this)
5✔
886
                this.esmtp = true
5✔
887
            }
5✔
888
        }
5✔
889
    }
5✔
890
    capabilities_respond() {
2✔
891
        this.respond(250, this.capabilities)
5✔
892
    }
5✔
893
    quit_respond(retval, msg) {
2✔
894
        this.respond(221, msg || `${this.local.host} ${cfg.message.close}`, () => {
5✔
895
            this.disconnect()
4✔
896
        })
5✔
897
    }
5✔
898
    vrfy_respond(retval, msg) {
2✔
899
        switch (retval) {
×
900
            case constants.deny:
×
901
                this.respond(550, msg || 'Access Denied', () => {
×
902
                    this.reset_transaction()
×
903
                })
×
904
                break
×
905
            case constants.denydisconnect:
×
906
                this.respond(550, msg || 'Access Denied', () => {
×
907
                    this.disconnect()
×
908
                })
×
909
                break
×
910
            case constants.denysoft:
×
911
                this.respond(450, msg || 'Lookup Failed', () => {
×
912
                    this.reset_transaction()
×
913
                })
×
914
                break
×
915
            case constants.denysoftdisconnect:
×
916
                this.respond(450, msg || 'Lookup Failed', () => {
×
917
                    this.disconnect()
×
918
                })
×
919
                break
×
920
            case constants.ok:
×
921
                this.respond(250, msg || 'User OK')
×
922
                break
×
923
            default:
×
924
                this.respond(252, "Just try sending a mail and we'll see how it turns out...")
×
925
        }
×
926
    }
×
927
    noop_respond(retval, msg) {
2✔
928
        switch (retval) {
×
929
            case constants.deny:
×
930
                this.respond(500, msg || 'Stop wasting my time')
×
931
                break
×
932
            case constants.denydisconnect:
×
933
                this.respond(500, msg || 'Stop wasting my time', () => {
×
934
                    this.disconnect()
×
935
                })
×
936
                break
×
937
            default:
×
938
                this.respond(250, 'OK')
×
939
        }
×
940
    }
×
941
    rset_respond() {
2✔
942
        this.respond(250, 'OK', () => {
×
943
            this.reset_transaction()
×
944
        })
×
945
    }
×
946
    mail_respond(retval, msg) {
2✔
947
        if (!this.transaction) {
3!
948
            this.logerror('mail_respond found no transaction!')
×
949
            return
×
950
        }
×
951
        const sender = this.transaction.mail_from
3✔
952
        const dmsg = `sender ${sender.format()}`
3✔
953
        this.lognotice(dmsg, {
3✔
954
            code: constants.translate(retval),
3✔
955
            msg: msg || '',
3✔
956
        })
3✔
957

3✔
958
        const store_results = (action) => {
3✔
959
            let addr = sender.format()
3✔
960
            if (addr.length > 2) {
3✔
961
                // all but null sender
3✔
962
                addr = addr.slice(1, -1) // trim off < >
3✔
963
            }
3✔
964
            this.transaction.results.add(
3✔
965
                { name: 'mail_from' },
3✔
966
                {
3✔
967
                    action,
3✔
968
                    code: constants.translate(retval),
3✔
969
                    address: addr,
3✔
970
                },
3✔
971
            )
3✔
972
        }
3✔
973

3✔
974
        switch (retval) {
3✔
975
            case constants.deny:
3!
976
                this.respond(550, msg || `${dmsg} denied`, () => {
×
977
                    store_results('reject')
×
978
                    this.reset_transaction()
×
979
                })
×
980
                break
×
981
            case constants.denydisconnect:
3!
982
                this.respond(550, msg || `${dmsg} denied`, () => {
×
983
                    store_results('reject')
×
984
                    this.disconnect()
×
985
                })
×
986
                break
×
987
            case constants.denysoft:
3!
988
                this.respond(450, msg || `${dmsg} denied`, () => {
×
989
                    store_results('tempfail')
×
990
                    this.reset_transaction()
×
991
                })
×
992
                break
×
993
            case constants.denysoftdisconnect:
3!
994
                this.respond(450, msg || `${dmsg} denied`, () => {
×
995
                    store_results('tempfail')
×
996
                    this.disconnect()
×
997
                })
×
998
                break
×
999
            default:
3✔
1000
                store_results('accept')
3✔
1001
                this.respond(250, msg || `${dmsg} OK`)
3✔
1002
        }
3✔
1003
    }
3✔
1004
    rcpt_incr(rcpt, action, msg, retval) {
2✔
1005
        this.transaction.rcpt_count[action]++
3✔
1006
        this.rcpt_count[action]++
3✔
1007

3✔
1008
        const addr = rcpt.format()
3✔
1009
        const recipient = {
3✔
1010
            address: addr.slice(1, -1),
3✔
1011
            action,
3✔
1012
        }
3✔
1013

3✔
1014
        if (msg && action !== 'accept') {
3!
1015
            if (typeof msg === 'object' && msg.constructor.name === 'DSN') {
×
1016
                recipient.msg = msg.reply
×
1017
                recipient.code = msg.code
×
1018
            } else {
×
1019
                recipient.msg = msg
×
1020
                recipient.code = constants.translate(retval)
×
1021
            }
×
1022
        }
×
1023

3✔
1024
        this.transaction.results.push({ name: 'rcpt_to' }, { recipient })
3✔
1025
    }
3✔
1026
    rcpt_ok_respond(retval, msg) {
2✔
1027
        if (!this.transaction) {
3!
1028
            this.results.add(this, {
×
1029
                err: 'rcpt_ok_respond found no transaction',
×
1030
            })
×
1031
            return
×
1032
        }
×
1033
        if (!msg) msg = this.last_rcpt_msg
3✔
1034
        const rcpt = this.transaction.rcpt_to[this.transaction.rcpt_to.length - 1]
3✔
1035
        const dmsg = `recipient ${rcpt.format()}`
3✔
1036
        // Log OK instead of CONT as this hook only runs if hook_rcpt returns OK
3✔
1037
        this.lognotice(dmsg, {
3✔
1038
            code: constants.translate(retval === constants.cont ? constants.ok : retval),
3!
1039
            msg: msg || '',
3✔
1040
            sender: this.transaction.mail_from.address,
3✔
1041
        })
3✔
1042
        switch (retval) {
3✔
1043
            case constants.deny:
3!
1044
                this.respond(550, msg || `${dmsg} denied`, () => {
×
1045
                    this.rcpt_incr(rcpt, 'reject', msg, retval)
×
1046
                    this.transaction.rcpt_to.pop()
×
1047
                })
×
1048
                break
×
1049
            case constants.denydisconnect:
3!
1050
                this.respond(550, msg || `${dmsg} denied`, () => {
×
1051
                    this.rcpt_incr(rcpt, 'reject', msg, retval)
×
1052
                    this.disconnect()
×
1053
                })
×
1054
                break
×
1055
            case constants.denysoft:
3!
1056
                this.respond(450, msg || `${dmsg} denied`, () => {
×
1057
                    this.rcpt_incr(rcpt, 'tempfail', msg, retval)
×
1058
                    this.transaction.rcpt_to.pop()
×
1059
                })
×
1060
                break
×
1061
            case constants.denysoftdisconnect:
3!
1062
                this.respond(450, msg || `${dmsg} denied`, () => {
×
1063
                    this.rcpt_incr(rcpt, 'tempfail', msg, retval)
×
1064
                    this.disconnect()
×
1065
                })
×
1066
                break
×
1067
            default:
3✔
1068
                this.respond(250, msg || `${dmsg} OK`, () => {
3✔
1069
                    this.rcpt_incr(rcpt, 'accept', msg, retval)
3✔
1070
                })
3✔
1071
        }
3✔
1072
    }
3✔
1073
    rcpt_respond(retval, msg) {
2✔
1074
        if (retval === constants.cont && this.relaying) {
5!
1075
            retval = constants.ok
×
1076
        }
×
1077

5✔
1078
        if (!this.transaction) {
5!
1079
            this.results.add(this, {
×
1080
                err: 'rcpt_respond found no transaction',
×
1081
            })
×
1082
            return
×
1083
        }
×
1084
        const rcpt = this.transaction.rcpt_to[this.transaction.rcpt_to.length - 1]
5✔
1085
        const dmsg = `recipient ${rcpt.format()}`
5✔
1086
        if (retval !== constants.ok) {
5✔
1087
            this.lognotice(dmsg, {
1✔
1088
                code: constants.translate(retval === constants.cont ? constants.ok : retval),
1✔
1089
                msg: msg || '',
1✔
1090
                sender: this.transaction.mail_from.address,
1✔
1091
            })
1✔
1092
        }
1✔
1093
        switch (retval) {
5✔
1094
            case constants.deny:
5✔
1095
                this.respond(550, msg || `${dmsg} denied`, () => {
1✔
1096
                    this.rcpt_incr(rcpt, 'reject', msg, retval)
1✔
1097
                    this.transaction.rcpt_to.pop()
1✔
1098
                })
1✔
1099
                break
1✔
1100
            case constants.denydisconnect:
5!
1101
                this.respond(550, msg || `${dmsg} denied`, () => {
×
1102
                    this.rcpt_incr(rcpt, 'reject', msg, retval)
×
1103
                    this.disconnect()
×
1104
                })
×
1105
                break
×
1106
            case constants.denysoft:
5!
1107
                this.respond(450, msg || `${dmsg} denied`, () => {
×
1108
                    this.rcpt_incr(rcpt, 'tempfail', msg, retval)
×
1109
                    this.transaction.rcpt_to.pop()
×
1110
                })
×
1111
                break
×
1112
            case constants.denysoftdisconnect:
5!
1113
                this.respond(450, msg || `${dmsg} denied`, () => {
×
1114
                    this.rcpt_incr(rcpt, 'tempfail', msg, retval)
×
1115
                    this.disconnect()
×
1116
                })
×
1117
                break
×
1118
            case constants.ok:
5✔
1119
                // Store any msg for rcpt_ok
1✔
1120
                this.last_rcpt_msg = msg
1✔
1121
                plugins.run_hooks('rcpt_ok', this, rcpt)
1✔
1122
                break
1✔
1123
            default: {
5!
1124
                if (retval !== constants.cont) {
×
1125
                    this.logalert('No plugin determined if relaying was allowed')
×
1126
                }
×
1127
                const rej_msg = `I cannot deliver mail for ${rcpt.format()}`
×
1128
                this.respond(550, rej_msg, () => {
×
1129
                    this.rcpt_incr(rcpt, 'reject', rej_msg, retval)
×
1130
                    this.transaction.rcpt_to.pop()
×
1131
                })
×
1132
            }
×
1133
        }
5✔
1134
    }
5✔
1135
    /////////////////////////////////////////////////////////////////////////////
2✔
1136
    // HAProxy support
2✔
1137

2✔
1138
    apply_proxy(proxy) {
2✔
1139
        if (this.proxy.timer) {
2!
1140
            clearTimeout(this.proxy.timer)
×
1141
            this.proxy.timer = null
×
1142
        }
×
1143

2✔
1144
        const { proto, src_ip, src_port, dst_ip, dst_port } = proxy
2✔
1145
        const proxy_ip = proxy.proxy_ip || this.remote.ip
2✔
1146

2✔
1147
        // Apply changes
2✔
1148
        this.loginfo('HAProxy', {
2✔
1149
            proto,
2✔
1150
            src_ip: `${src_ip}:${src_port}`,
2✔
1151
            dst_ip: `${dst_ip}:${dst_port}`,
2✔
1152
        })
2✔
1153

2✔
1154
        this.notes.proxy = {
2✔
1155
            type: 'haproxy',
2✔
1156
            proto,
2✔
1157
            src_ip,
2✔
1158
            src_port,
2✔
1159
            dst_ip,
2✔
1160
            dst_port,
2✔
1161
            proxy_ip,
2✔
1162
        }
2✔
1163

2✔
1164
        this.reset_transaction(() => {
2✔
1165
            this.set('proxy.ip', proxy_ip)
2✔
1166
            this.set('proxy.type', 'haproxy')
2✔
1167
            this.relaying = false
2✔
1168
            this.set('local.ip', dst_ip)
2✔
1169
            this.set('local.port', parseInt(dst_port, 10))
2✔
1170
            this.set('remote.ip', src_ip)
2✔
1171
            this.set('remote.port', parseInt(src_port, 10))
2✔
1172
            this.set('remote.host', null)
2✔
1173
            this.set('hello.host', null)
2✔
1174
            plugins.run_hooks('connect_init', this)
2✔
1175
        })
2✔
1176
    }
2✔
1177

2✔
1178
    cmd_proxy(line) {
2✔
1179
        if (!this.proxy.allowed) {
2✔
1180
            this.respond(421, `PROXY not allowed from ${this.remote.ip}`)
1✔
1181
            return this.disconnect()
1✔
1182
        }
1✔
1183

1✔
1184
        const proxy = net_utils.parse_proxy_line(line)
1✔
1185
        if (!proxy) {
2!
1186
            this.respond(421, 'Invalid PROXY format')
×
1187
            return this.disconnect()
×
1188
        }
✔
1189

1✔
1190
        this.apply_proxy(proxy)
1✔
1191
    }
2✔
1192
    /////////////////////////////////////////////////////////////////////////////
2✔
1193
    // SMTP Commands
2✔
1194

2✔
1195
    cmd_internalcmd(line) {
2✔
1196
        if (!this.remote.is_local) {
×
1197
            return this.respond(501, 'INTERNALCMD not allowed remotely')
×
1198
        }
×
1199
        const results = String(line).split(/ +/)
×
1200
        if (/key:/.test(results[0])) {
×
1201
            const internal_key = config.get('internalcmd_key')
×
1202
            if (results[0] != `key:${internal_key}`) {
×
1203
                return this.respond(501, 'Invalid internalcmd_key - check config')
×
1204
            }
×
1205
            results.shift()
×
1206
        } else if (config.get('internalcmd_key')) {
×
1207
            return this.respond(501, 'Missing internalcmd_key - check config')
×
1208
        }
×
1209

×
1210
        // Now send the internal command to the master process
×
1211
        const command = results.shift()
×
1212
        if (!command) {
×
1213
            return this.respond(501, 'No command given')
×
1214
        }
×
1215

×
1216
        require('./server').sendToMaster(command, results)
×
1217
        return this.respond(250, 'Command sent for execution. Check Haraka logs for results.')
×
1218
    }
×
1219
    cmd_helo(line) {
2✔
1220
        const results = String(line).split(/ +/)
1✔
1221
        const host = results[0]
1✔
1222
        if (!host) {
1!
1223
            return this.respond(501, 'HELO requires domain/address - see RFC-2821 4.1.1.1')
×
1224
        }
×
1225
        // RFC 5321 §4.1.1.1: the domain/address-literal cannot contain
1✔
1226
        // control characters. process_line() only strips the first \r?\n,
1✔
1227
        // so a bare \r could otherwise survive into hello.host and the
1✔
1228
        // generated Received: header / logs (header injection).
1✔
1229
        if (/[\x00-\x1f\x7f]/.test(host)) {
1✔
1230
            return this.respond(501, 'HELO syntax error - see RFC-2821 4.1.1.1')
1✔
1231
        }
1!
1232

×
1233
        this.reset_transaction(() => {
×
1234
            this.set('hello', 'verb', 'HELO')
×
1235
            this.set('hello', 'host', host)
×
1236
            this.results.add({ name: 'helo' }, this.hello)
×
1237
            plugins.run_hooks('helo', this, host)
×
1238
        })
×
1239
    }
1✔
1240
    cmd_ehlo(line) {
2✔
1241
        const results = String(line).split(/ +/)
6✔
1242
        const host = results[0]
6✔
1243
        if (!host) {
6!
1244
            return this.respond(501, 'EHLO requires domain/address - see RFC-2821 4.1.1.1')
×
1245
        }
×
1246
        // RFC 5321 §4.1.1.1: reject control chars (see cmd_helo).
6✔
1247
        if (/[\x00-\x1f\x7f]/.test(host)) {
6✔
1248
            return this.respond(501, 'EHLO syntax error - see RFC-2821 4.1.1.1')
6✔
1249
        }
6✔
1250

6✔
1251
        this.reset_transaction(() => {
6✔
1252
            this.set('hello', 'verb', 'EHLO')
5✔
1253
            this.set('hello', 'host', host)
5✔
1254
            this.results.add({ name: 'helo' }, this.hello)
5✔
1255
            plugins.run_hooks('ehlo', this, host)
5✔
1256
        })
6✔
1257
    }
6✔
1258
    cmd_quit(args) {
2✔
1259
        // RFC 5321 Section 4.3.2
5✔
1260
        // QUIT does not accept arguments
5✔
1261
        if (args) {
5!
1262
            return this.respond(501, 'Syntax error')
×
1263
        }
×
1264
        plugins.run_hooks('quit', this)
5✔
1265
    }
5✔
1266
    cmd_rset(args) {
2✔
1267
        // RFC 5321 Section 4.3.2
×
1268
        // RSET does not accept arguments
×
1269
        if (args) {
×
1270
            return this.respond(501, 'Syntax error')
×
1271
        }
×
1272
        plugins.run_hooks('rset', this)
×
1273
    }
×
1274
    cmd_vrfy() {
2✔
1275
        // only supported via plugins
×
1276
        plugins.run_hooks('vrfy', this)
×
1277
    }
×
1278
    cmd_noop() {
2✔
1279
        plugins.run_hooks('noop', this)
×
1280
    }
×
1281
    cmd_help() {
2✔
1282
        this.respond(250, 'Not implemented')
×
1283
    }
×
1284
    cmd_mail(line) {
2✔
1285
        if (!this.hello.host) {
3!
1286
            this.errors++
×
1287
            return this.respond(503, 'Use EHLO/HELO before MAIL')
×
1288
        }
×
1289
        // Require authentication on ports 587 & 465
3✔
1290
        if (!this.relaying && [587, 465].includes(this.local.port)) {
3!
1291
            this.errors++
×
1292
            return this.respond(550, 'Authentication required')
×
1293
        }
×
1294

3✔
1295
        let results
3✔
1296
        try {
3✔
1297
            results = rfc1869.parse('mail', line, !this.relaying && cfg.main.strict_rfc1869)
3✔
1298
        } catch (err) {
3!
1299
            this.errors++
×
1300
            if (err.stack) {
×
1301
                this.lognotice(err.stack.split(/\n/)[0])
×
1302
            } else {
×
1303
                this.logerror(err)
×
1304
            }
×
1305
            // Explicitly handle out-of-disk space errors
×
1306
            if (err.code === 'ENOSPC') {
×
1307
                return this.respond(452, 'Internal Server Error')
×
1308
            } else {
×
1309
                return this.respond(501, ['Command parsing failed', err])
×
1310
            }
×
1311
        }
×
1312

3✔
1313
        let from
3✔
1314
        const from_raw = results.shift()
3✔
1315
        try {
3✔
1316
            from = new Address(from_raw)
3✔
1317
        } catch (err) {
3!
NEW
1318
            const msg = `Invalid MAIL FROM address ${JSON.stringify(from_raw)}: ${err.message}`
×
NEW
1319
            this.lognotice(msg)
×
NEW
1320
            return this.respond(501, msg)
×
UNCOV
1321
        }
×
1322

3✔
1323
        // Get rest of key=value pairs
3✔
1324
        const params = {}
3✔
1325
        for (const param of results) {
3!
1326
            const kv = param.match(/^([^=]+)(?:=(.+))?$/)
×
1327
            if (kv) params[kv[1].toUpperCase()] = kv[2] || null
×
1328
        }
×
1329

3✔
1330
        // Parameters are only valid if EHLO was sent
3✔
1331
        if (!this.esmtp && Object.keys(params).length > 0) {
3!
1332
            return this.respond(555, 'Invalid command parameters')
×
1333
        }
×
1334

3✔
1335
        // Handle SIZE extension
3✔
1336
        if (params?.SIZE && params.SIZE > 0) {
3!
1337
            if (cfg.max.bytes > 0 && params.SIZE > cfg.max.bytes) {
×
1338
                return this.respond(550, 'Message too big!')
×
1339
            }
×
1340
        }
×
1341

3✔
1342
        this.init_transaction(() => {
3✔
1343
            this.transaction.mail_from = from
3✔
1344
            if (this.hello.verb == 'HELO') {
3!
1345
                this.transaction.encoding = 'binary'
×
1346
                this.encoding = 'binary'
×
1347
            }
×
1348
            plugins.run_hooks('mail', this, [from, params])
3✔
1349
        })
3✔
1350
    }
3✔
1351
    cmd_rcpt(line) {
2✔
1352
        if (!this.transaction || !this.transaction.mail_from) {
3!
1353
            this.errors++
×
1354
            return this.respond(503, 'Use MAIL before RCPT')
×
1355
        }
×
1356

3✔
1357
        let results
3✔
1358
        try {
3✔
1359
            results = rfc1869.parse('rcpt', line, cfg.main.strict_rfc1869 && !this.relaying)
3!
1360
        } catch (err) {
3!
1361
            this.errors++
×
1362
            if (err.stack) {
×
1363
                this.lognotice(err.stack.split(/\n/)[0])
×
1364
            } else {
×
1365
                this.logerror(err)
×
1366
            }
×
1367
            // Explicitly handle out-of-disk space errors
×
1368
            if (err.code === 'ENOSPC') {
×
1369
                return this.respond(452, 'Internal Server Error')
×
1370
            } else {
×
1371
                return this.respond(501, ['Command parsing failed', err])
×
1372
            }
×
1373
        }
×
1374

3✔
1375
        let recip
3✔
1376
        const recip_raw = results.shift()
3✔
1377
        try {
3✔
1378
            recip = new Address(recip_raw)
3✔
1379
        } catch (err) {            
3!
NEW
1380
            const msg = `Invalid RCPT TO address ${JSON.stringify(recip_raw)}: ${err.message}`
×
NEW
1381
            this.lognotice(msg)
×
NEW
1382
            return this.respond(501, msg)
×
UNCOV
1383
        }
×
1384

3✔
1385
        // Get rest of key=value pairs
3✔
1386
        const params = {}
3✔
1387
        for (const param of results) {
3!
1388
            const kv = param.match(/^([^=]+)(?:=(.+))?$/)
×
1389
            if (kv) params[kv[1].toUpperCase()] = kv[2] || null
×
1390
        }
×
1391

3✔
1392
        // Parameters are only valid if EHLO was sent
3✔
1393
        if (!this.esmtp && Object.keys(params).length > 0) {
3!
1394
            return this.respond(555, 'Invalid command parameters')
×
1395
        }
×
1396

3✔
1397
        this.transaction.rcpt_to.push(recip)
3✔
1398
        plugins.run_hooks('rcpt', this, [recip, params])
3✔
1399
    }
3✔
1400
    received_line() {
2✔
1401
        let smtp = this.hello.verb === 'EHLO' ? 'ESMTP' : 'SMTP'
3!
1402
        // Implement RFC3848
3✔
1403
        if (this.tls.enabled) smtp += 'S'
3!
1404
        if (this.authheader) smtp += 'A'
3✔
1405

3✔
1406
        let sslheader
3✔
1407

3✔
1408
        if (this.get('tls.cipher.version')) {
3!
1409
            // standardName appeared in Node.js v12.16 and v13.4
×
1410
            // RFC 8314
×
1411
            sslheader = `tls ${this.tls.cipher.standardName || this.tls.cipher.name}`
×
1412
        }
×
1413

3✔
1414
        let received_header = `from ${this.hello.host} (${this.get_remote('info')})\r
3✔
1415
\tby ${this.local.host} (${this.local.info}) with ${smtp} id ${this.transaction.uuid}\r
3✔
1416
\tenvelope-from ${this.transaction.mail_from.format()}`
3✔
1417

3✔
1418
        if (sslheader) received_header += `\r\n\t${sslheader.replace(/\r?\n\t?$/, '')}`
3!
1419

3✔
1420
        // Does not follow RFC 5321 section 4.4 grammar
3✔
1421
        if (this.authheader) received_header += ` ${this.authheader.replace(/\r?\n\t?$/, '')}`
3✔
1422

3✔
1423
        received_header += `;\r\n\t${utils.date_to_str(new Date())}`
3✔
1424

3✔
1425
        return received_header
3✔
1426
    }
3✔
1427
    auth_results(message) {
2✔
1428
        // https://datatracker.ietf.org/doc/rfc7001/
9✔
1429
        const has_tran = !!this.transaction?.notes
9✔
1430

9✔
1431
        // initialize connection note
9✔
1432
        if (!this.notes.authentication_results) {
9✔
1433
            this.notes.authentication_results = []
4✔
1434
        }
4✔
1435

9✔
1436
        // initialize transaction note, if possible
9✔
1437
        if (has_tran === true && !this.transaction.notes.authentication_results) {
9✔
1438
            this.transaction.notes.authentication_results = []
3✔
1439
        }
3✔
1440

9✔
1441
        // Strip CR/LF and other control chars: an attacker-influenced
9✔
1442
        // value (e.g. a failed AUTH username, see auth_base) must not be
9✔
1443
        // able to inject extra header lines into Authentication-Results.
9✔
1444
        // The legitimate folding (;\r\n\t) is added by the join below.
9✔
1445
        const ar_clean = (s) => String(s).replace(/[\x00-\x1f\x7f]/g, '')
9✔
1446

9✔
1447
        // if message, store it in the appropriate note
9✔
1448
        if (message) {
9✔
1449
            if (has_tran === true) {
2!
1450
                this.transaction.notes.authentication_results.push(ar_clean(message))
×
1451
            } else {
2✔
1452
                this.notes.authentication_results.push(ar_clean(message))
2✔
1453
            }
2✔
1454
        }
2✔
1455

9✔
1456
        // assemble the new header
9✔
1457
        let header = [ar_clean(this.local.host), ...this.notes.authentication_results]
9✔
1458
        if (has_tran === true) {
9✔
1459
            header = [...header, ...this.transaction.notes.authentication_results]
6✔
1460
        }
6✔
1461
        if (header.length === 1) return '' // no results
9✔
1462
        return header.join(';\r\n\t')
4✔
1463
    }
9✔
1464
    auth_results_clean() {
2✔
1465
        // move any existing Auth-Res headers to Original-Auth-Res headers
3✔
1466
        // http://tools.ietf.org/html/draft-kucherawy-original-authres-00.html
3✔
1467
        const ars = this.transaction.header.get_all('Authentication-Results')
3✔
1468
        if (ars.length === 0) return
3!
1469

×
1470
        for (const element of ars) {
×
1471
            this.transaction.add_header('Original-Authentication-Results', element)
×
1472
        }
×
1473
        this.transaction.remove_header('Authentication-Results')
×
1474
        this.logdebug('Authentication-Results moved to Original-Authentication-Results')
×
1475
    }
3✔
1476
    cmd_data(args) {
2✔
1477
        // RFC 5321 Section 4.3.2
6✔
1478
        // DATA does not accept arguments
6✔
1479
        if (args) {
6✔
1480
            this.errors++
1✔
1481
            return this.respond(501, 'Syntax error')
1✔
1482
        }
1✔
1483
        if (!this.transaction) {
6✔
1484
            this.errors++
1✔
1485
            return this.respond(503, 'MAIL required first')
1✔
1486
        }
1✔
1487
        if (!this.transaction.rcpt_to.length) {
1✔
1488
            if (this.pipelining) {
1!
1489
                return this.respond(554, 'No valid recipients')
×
1490
            }
×
1491
            this.errors++
1✔
1492
            return this.respond(503, 'RCPT required first')
1✔
1493
        }
1✔
1494

6✔
1495
        if (cfg.headers.add_received) {
6✔
1496
            this.accumulate_data(`Received: ${this.received_line()}\r\n`)
6✔
1497
        }
6✔
1498
        plugins.run_hooks('data', this)
6✔
1499
    }
6✔
1500
    data_respond(retval, msg) {
2✔
1501
        let cont = 0
5✔
1502
        switch (retval) {
5✔
1503
            case constants.deny:
5!
1504
                this.respond(554, msg || 'Message denied', () => {
×
1505
                    this.reset_transaction()
×
1506
                })
×
1507
                break
×
1508
            case constants.denydisconnect:
5!
1509
                this.respond(554, msg || 'Message denied', () => {
×
1510
                    this.disconnect()
×
1511
                })
×
1512
                break
×
1513
            case constants.denysoft:
5!
1514
                this.respond(451, msg || 'Message denied', () => {
×
1515
                    this.reset_transaction()
×
1516
                })
×
1517
                break
×
1518
            case constants.denysoftdisconnect:
5✔
1519
                this.respond(451, msg || 'Message denied', () => {
1✔
1520
                    this.disconnect()
1✔
1521
                })
1✔
1522
                break
1✔
1523
            default:
5✔
1524
                cont = 1
1✔
1525
        }
5✔
1526
        if (!cont) return
5✔
1527

1✔
1528
        // We already checked for MAIL/RCPT in cmd_data
1✔
1529
        this.respond(354, 'go ahead, make my day', () => {
1✔
1530
            // OK... now we get the data
4✔
1531
            this.state = states.DATA
4✔
1532
            this.transaction.data_bytes = 0
4✔
1533
        })
1✔
1534
    }
5✔
1535
    accumulate_data(line) {
2✔
1536
        this.transaction.data_bytes += line.length
17✔
1537

17✔
1538
        // Look for .\r\n
17✔
1539
        if (line.length === 3 && line[0] === 0x2e && line[1] === 0x0d && line[2] === 0x0a) {
17✔
1540
            this.data_done()
3✔
1541
            return
3✔
1542
        }
3✔
1543

14✔
1544
        // Look for .\n
14✔
1545
        if (line.length === 2 && line[0] === 0x2e && line[1] === 0x0a) {
17!
1546
            this.lognotice('Client sent bare line-feed - .\\n rather than .\\r\\n')
×
1547
            this.respond(451, 'Bare line-feed; see http://haraka.github.io/barelf/', () => {
×
1548
                this.reset_transaction()
×
1549
            })
×
1550
            return
×
1551
        }
✔
1552

14✔
1553
        // Stop accumulating data as we're going to reject at dot.
14✔
1554
        if (cfg.max.bytes && this.transaction.data_bytes > cfg.max.bytes) {
17!
1555
            return
×
1556
        }
✔
1557

14✔
1558
        if (this.transaction.mime_part_count >= cfg.max.mime_parts) {
17!
1559
            this.logcrit('Possible DoS attempt - too many MIME parts')
×
1560
            this.respond(554, 'Transaction failed due to too many MIME parts', () => {
×
1561
                this.disconnect()
×
1562
            })
×
1563
            return
×
1564
        }
✔
1565

14✔
1566
        this.transaction.add_data(line)
14✔
1567
    }
17✔
1568
    data_done() {
2✔
1569
        this.pause()
3✔
1570
        this.totalbytes += this.transaction.data_bytes
3✔
1571

3✔
1572
        // Check message size limit
3✔
1573
        if (cfg.max.bytes && this.transaction.data_bytes > cfg.max.bytes) {
3!
1574
            this.lognotice(`Incoming message exceeded max size of ${cfg.max.bytes}`)
×
1575
            return plugins.run_hooks('max_data_exceeded', this)
×
1576
        }
×
1577

3✔
1578
        // Check max received headers count
3✔
1579
        if (this.transaction.header.get_all('received').length > cfg.headers.max_received) {
3!
1580
            this.logerror('Incoming message had too many Received headers')
×
1581
            this.respond(550, 'Too many received headers - possible mail loop', () => {
×
1582
                this.reset_transaction()
×
1583
            })
×
1584
            return
×
1585
        }
×
1586

3✔
1587
        // Warn if we hit the maximum parsed header lines limit
3✔
1588
        if (this.transaction.header_lines.length >= cfg.headers.max_lines) {
3!
1589
            this.logwarn(`Incoming message reached maximum parsing limit of ${cfg.headers.max_lines} header lines`)
×
1590
        }
×
1591

3✔
1592
        if (cfg.headers.clean_auth_results) {
3✔
1593
            this.auth_results_clean() // rename old A-R headers
3✔
1594
        }
3✔
1595
        const ar_field = this.auth_results() // assemble new one
3✔
1596
        if (ar_field) {
3✔
1597
            this.transaction.add_header('Authentication-Results', ar_field)
1✔
1598
        }
1✔
1599

3✔
1600
        this.transaction.end_data(() => {
3✔
1601
            // As this will be called asynchronously,
3✔
1602
            // make sure we still have a transaction.
3✔
1603
            if (!this.transaction) return
3!
1604
            // Record the start time of this hook as we can't take too long
3✔
1605
            // as the client will typically hang up after 2 to 3 minutes
3✔
1606
            // despite the RFC mandating that 10 minutes should be allowed.
3✔
1607
            this.transaction.data_post_start = Date.now()
3✔
1608
            plugins.run_hooks('data_post', this)
3✔
1609
        })
3✔
1610
    }
3✔
1611
    data_post_respond(retval, msg) {
2✔
1612
        if (!this.transaction) return
3!
1613
        this.transaction.data_post_delay = (Date.now() - this.transaction.data_post_start) / 1000
3✔
1614
        const mid = this.transaction.header.get('Message-ID') || ''
3✔
1615
        this.lognotice('message', {
3✔
1616
            mid: mid.replace(/\r?\n/, ''),
3✔
1617
            size: this.transaction.data_bytes,
3✔
1618
            rcpts: `${this.transaction.rcpt_count.accept}/${this.transaction.rcpt_count.tempfail}/${this.transaction.rcpt_count.reject}`,
3✔
1619
            delay: this.transaction.data_post_delay,
3✔
1620
            code: constants.translate(retval),
3✔
1621
            msg: msg || '',
3✔
1622
        })
3✔
1623
        const ar_field = this.auth_results() // assemble A-R header
3✔
1624
        if (ar_field) {
3✔
1625
            this.transaction.remove_header('Authentication-Results')
1✔
1626
            this.transaction.add_leading_header('Authentication-Results', ar_field)
1✔
1627
        }
1✔
1628
        switch (retval) {
3✔
1629
            case constants.deny:
3!
1630
                this.respond(550, msg || 'Message denied', () => {
×
1631
                    this.msg_count.reject++
×
1632
                    this.transaction.msg_status = 'rejected'
×
1633
                    this.reset_transaction(() => this.resume())
×
1634
                })
×
1635
                break
×
1636
            case constants.denydisconnect:
3!
1637
                this.respond(550, msg || 'Message denied', () => {
×
1638
                    this.msg_count.reject++
×
1639
                    this.transaction.msg_status = 'rejected'
×
1640
                    this.disconnect()
×
1641
                })
×
1642
                break
×
1643
            case constants.denysoft:
3!
1644
                this.respond(450, msg || 'Message denied temporarily', () => {
×
1645
                    this.msg_count.tempfail++
×
1646
                    this.transaction.msg_status = 'deferred'
×
1647
                    this.reset_transaction(() => this.resume())
×
1648
                })
×
1649
                break
×
1650
            case constants.denysoftdisconnect:
3!
1651
                this.respond(450, msg || 'Message denied temporarily', () => {
×
1652
                    this.msg_count.tempfail++
×
1653
                    this.transaction.msg_status = 'deferred'
×
1654
                    this.disconnect()
×
1655
                })
×
1656
                break
×
1657
            default:
3✔
1658
                if (this.relaying) {
3✔
1659
                    plugins.run_hooks('queue_outbound', this)
1✔
1660
                } else {
3✔
1661
                    plugins.run_hooks('queue', this)
2✔
1662
                }
2✔
1663
        }
3✔
1664
    }
3✔
1665
    max_data_exceeded_respond(retval) {
2✔
1666
        // TODO: Maybe figure out what to do with other return codes
×
1667
        this.respond(retval === constants.denysoft ? 450 : 550, 'Message too big!', () => {
×
1668
            this.reset_transaction()
×
1669
        })
×
1670
    }
×
1671
    queue_msg(retval, msg) {
2✔
1672
        if (msg) {
13✔
1673
            if (typeof msg === 'object' && msg.constructor.name === 'DSN') {
1✔
1674
                return msg.reply
1✔
1675
            }
1✔
1676
            return msg
1✔
1677
        }
1✔
1678

9✔
1679
        switch (retval) {
9✔
1680
            case constants.ok:
13✔
1681
                return 'Message Queued'
13✔
1682
            case constants.deny:
13✔
1683
            case constants.denydisconnect:
13✔
1684
                return 'Message denied'
3✔
1685
            case constants.denysoft:
13✔
1686
            case constants.denysoftdisconnect:
13✔
1687
                return 'Message denied temporarily'
4✔
1688
            default:
13✔
1689
                return ''
2✔
1690
        }
13✔
1691
    }
13✔
1692
    store_queue_result(retval, msg) {
2✔
1693
        const res_as = { name: 'queue' }
7✔
1694
        switch (retval) {
7✔
1695
            case constants.ok:
7✔
1696
                this.transaction.results.add(res_as, { pass: msg })
7✔
1697
                break
7✔
1698
            case constants.deny:
7!
1699
            case constants.denydisconnect:
7✔
1700
            case constants.denysoft:
7✔
1701
            case constants.denysoftdisconnect:
7✔
1702
                this.transaction.results.add(res_as, { fail: msg })
3✔
1703
                break
3✔
1704
            case constants.cont:
7✔
1705
                break
1✔
1706
            default:
7!
1707
                this.transaction.results.add(res_as, { msg })
×
1708
                break
×
1709
        }
7✔
1710
    }
7✔
1711
    queue_outbound_respond(retval, msg) {
2✔
1712
        if (this.remote.closed) return
1!
1713
        msg = this.queue_msg(retval, msg) || 'Message Queued'
1!
1714
        this.store_queue_result(retval, msg)
1✔
1715
        msg = `${msg} (${this.transaction.uuid})`
1✔
1716
        if (retval !== constants.ok) {
1!
1717
            this.lognotice('queue', {
×
1718
                code: constants.translate(retval),
×
1719
                msg,
×
1720
            })
×
1721
        }
×
1722
        switch (retval) {
1✔
1723
            case constants.ok:
1✔
1724
                plugins.run_hooks('queue_ok', this, msg)
1✔
1725
                break
1✔
1726
            case constants.deny:
1!
1727
                this.respond(550, msg, () => {
×
1728
                    this.msg_count.reject++
×
1729
                    this.transaction.msg_status = 'rejected'
×
1730
                    this.reset_transaction(() => this.resume())
×
1731
                })
×
1732
                break
×
1733
            case constants.denydisconnect:
1!
1734
                this.respond(550, msg, () => {
×
1735
                    this.msg_count.reject++
×
1736
                    this.transaction.msg_status = 'rejected'
×
1737
                    this.disconnect()
×
1738
                })
×
1739
                break
×
1740
            case constants.denysoft:
1!
1741
                this.respond(450, msg, () => {
×
1742
                    this.msg_count.tempfail++
×
1743
                    this.transaction.msg_status = 'deferred'
×
1744
                    this.reset_transaction(() => this.resume())
×
1745
                })
×
1746
                break
×
1747
            case constants.denysoftdisconnect:
1!
1748
                this.respond(450, msg, () => {
×
1749
                    this.msg_count.tempfail++
×
1750
                    this.transaction.msg_status = 'deferred'
×
1751
                    this.disconnect()
×
1752
                })
×
1753
                break
×
1754
            default:
1!
1755
                outbound.send_trans_email(this.transaction, (retval2, msg2) => {
×
1756
                    if (!msg2) msg2 = this.queue_msg(retval2, msg)
×
1757
                    switch (retval2) {
×
1758
                        case constants.ok:
×
1759
                            if (!msg2) msg2 = this.queue_msg(retval2, msg2)
×
1760
                            plugins.run_hooks('queue_ok', this, msg2)
×
1761
                            break
×
1762
                        case constants.deny:
×
1763
                            if (!msg2) msg2 = this.queue_msg(retval2, msg2)
×
1764
                            this.respond(550, msg2, () => {
×
1765
                                this.msg_count.reject++
×
1766
                                this.transaction.msg_status = 'rejected'
×
1767
                                this.reset_transaction(() => {
×
1768
                                    this.resume()
×
1769
                                })
×
1770
                            })
×
1771
                            break
×
1772
                        default:
×
1773
                            this.logerror(`Unrecognized response from outbound layer: ${retval2} : ${msg2}`)
×
1774
                            this.respond(550, msg2 || 'Internal Server Error', () => {
×
1775
                                this.msg_count.reject++
×
1776
                                this.transaction.msg_status = 'rejected'
×
1777
                                this.reset_transaction(() => {
×
1778
                                    this.resume()
×
1779
                                })
×
1780
                            })
×
1781
                    }
×
1782
                })
×
1783
        }
1✔
1784
    }
1✔
1785
    queue_respond(retval, msg) {
2✔
1786
        msg = this.queue_msg(retval, msg)
6✔
1787
        this.store_queue_result(retval, msg)
6✔
1788
        msg = `${msg} (${this.transaction.uuid})`
6✔
1789

6✔
1790
        if (retval !== constants.ok) {
6✔
1791
            this.lognotice('queue', {
6✔
1792
                code: constants.translate(retval),
6✔
1793
                msg,
6✔
1794
            })
6✔
1795
        }
6✔
1796
        switch (retval) {
6✔
1797
            case constants.ok:
6✔
1798
                plugins.run_hooks('queue_ok', this, msg)
6✔
1799
                break
6✔
1800
            case constants.deny:
6!
1801
                this.respond(550, msg, () => {
×
1802
                    this.msg_count.reject++
×
1803
                    this.transaction.msg_status = 'rejected'
×
1804
                    this.reset_transaction(() => this.resume())
×
1805
                })
×
1806
                break
×
1807
            case constants.denydisconnect:
6✔
1808
                this.respond(550, msg, () => {
1✔
1809
                    this.msg_count.reject++
1✔
1810
                    this.transaction.msg_status = 'rejected'
1✔
1811
                    this.disconnect()
1✔
1812
                })
1✔
1813
                break
1✔
1814
            case constants.denysoft:
6✔
1815
                this.respond(450, msg, () => {
1✔
1816
                    this.msg_count.tempfail++
1✔
1817
                    this.transaction.msg_status = 'deferred'
1✔
1818
                    this.reset_transaction(() => this.resume())
1✔
1819
                })
1✔
1820
                break
1✔
1821
            case constants.denysoftdisconnect:
6✔
1822
                this.respond(450, msg, () => {
1✔
1823
                    this.msg_count.tempfail++
1✔
1824
                    this.transaction.msg_status = 'deferred'
1✔
1825
                    this.disconnect()
1✔
1826
                })
1✔
1827
                break
1✔
1828
            default:
6✔
1829
                if (!msg) msg = 'Queuing declined or disabled, try later'
1✔
1830
                this.respond(451, msg, () => {
1✔
1831
                    this.msg_count.tempfail++
1✔
1832
                    this.transaction.msg_status = 'deferred'
1✔
1833
                    this.reset_transaction(() => this.resume())
1✔
1834
                })
1✔
1835
                break
1✔
1836
        }
6✔
1837
    }
6✔
1838
    queue_ok_respond(retval, msg, params) {
2✔
1839
        // This hook is common to both hook_queue and hook_queue_outbound
4✔
1840
        // retval and msg are ignored in this hook so we always log OK
4✔
1841
        this.lognotice('queue', {
4✔
1842
            code: 'OK',
4✔
1843
            msg: params || '',
4!
1844
        })
4✔
1845

4✔
1846
        this.respond(250, params, () => {
4✔
1847
            this.msg_count.accept++
4✔
1848
            if (this.transaction) this.transaction.msg_status = 'accepted'
4✔
1849
            this.reset_transaction(() => this.resume())
4✔
1850
        })
4✔
1851
    }
4✔
1852
}
2✔
1853

2✔
1854
exports.Connection = Connection
2✔
1855

2✔
1856
exports.createConnection = (client, server, cfg) => {
2✔
1857
    return new Connection(client, server, cfg)
73✔
1858
}
73✔
1859

2✔
1860
logger.add_log_methods(Connection)
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc