Ctrl K
ComunidadGuías…

connection.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
'use strict'
// a single connection

const dns = require('node:dns')
const os = require('node:os')

// npm libs
const ipaddr = require('ipaddr.js')
const config = require('haraka-config')
const constants = require('haraka-constants')
const net_utils = require('haraka-net-utils')
const Notes = require('haraka-notes')
const utils = require('haraka-utils')
const { Address } = require('@haraka/email-address')
const ResultStore = require('haraka-results')

// Haraka libs
const logger = require('./logger')
const trans = require('./transaction')
const plugins = require('./plugins')
const rfc1869 = utils.rfc1869
const outbound = require('./outbound')

const states = constants.connection.state
const version = utils.getVersion(__dirname)

const MAX_POST_DATA_BYTES = 8192

const cfg = config.get('connection.ini', {
    booleans: [
        '-main.strict_rfc1869',
        '-main.postel',
        '+main.smtputf8',
        '+headers.add_received',
        '+headers.show_version',
        '+headers.clean_auth_results',
    ],
})

// Count LF-terminated commands in the post-DATA buffer, which process_data() has
// already bounded to MAX_POST_DATA_BYTES. Every complete command is counted so
// respond() can answer each with a 503 and keep the client's response count in sync.
function count_complete_commands(buf) {
    let count = 0
    let rest = buf
    let offset
    while ((offset = utils.indexOfLF(rest)) !== -1) {
        count++
        rest = rest.slice(offset + 1)
    }
    return count
}

class Connection {
    constructor(client, server) {
        this.client = client
        this.server = server

        this.local = {
            ip: null,
            port: null,
            host: net_utils.get_primary_host_name(),
            info: 'Haraka',
        }
        this.remote = {
            ip: null,
            port: null,
            host: null,
            info: null,
            closed: false,
            is_private: false,
            is_local: false,
        }
        this.hello = {
            host: null,
            verb: null,
        }
        this.tls = {
            enabled: false,
            advertised: false,
            verified: false,
            cipher: {},
        }
        this.proxy = {
            allowed: false,
            ip: null,
            type: null,
            timer: null,
        }
        this.set('tls', 'enabled', !!server.has_tls)

        this.current_data = null
        this.current_line = null
        this.state = states.PAUSE
        this.encoding = 'utf8'
        this.prev_state = null
        this.awaiting_data_reply = false
        this.loop_code = null
        this.loop_msg = null
        this.uuid = utils.uuid()
        this.notes = new Notes()
        this.transaction = null
        this.tran_count = 0
        this.capabilities = null
        this.early_talker = false
        this.pipelining = false
        this._relaying = false
        this.esmtp = false
        this.last_response = null
        this.hooks_to_run = []
        this.start_time = Date.now()
        this.last_reject = ''
        this.totalbytes = 0
        this.rcpt_count = {
            accept: 0,
            tempfail: 0,
            reject: 0,
        }
        this.msg_count = {
            accept: 0,
            tempfail: 0,
            reject: 0,
        }
        this.results = new ResultStore(this)
        this.errors = 0
        this.last_rcpt_msg = null
        this.hook = null
        if (cfg.headers.show_version) {
            this.local.info += `/${version}`
        }
        Connection.setupClient(this)
    }
    static setupClient(self) {
        const ip = self.client.remoteAddress
        if (!ip) {
            self.logdebug('setupClient got no IP address for this connection!')
            self.client.destroy()
            return
        }

        const local_addr = self.server.address()
        self.set('local', 'ip', ipaddr.process(self.client.localAddress || local_addr.address).toString())
        self.set('local', 'port', self.client.localPort || local_addr.port)
        self.results.add({ name: 'local' }, self.local)

        self.set('remote', 'ip', ipaddr.process(ip).toString())
        self.set('remote', 'port', self.client.remotePort)
        self.results.add({ name: 'remote' }, self.remote)

        self.lognotice('connect', {
            ip: self.remote.ip,
            port: self.remote.port,
            local_ip: self.local.ip,
            local_port: self.local.port,
        })

        if (!self.client.on) return

        const log_data = { ip: self.remote.ip }
        if (self.remote.host) log_data.host = self.remote.host

        self.client.on('end', () => {
            if (self.state >= states.DISCONNECTING) return
            self.remote.closed = true
            self.loginfo('client half closed connection', log_data)
            self.fail()
        })

        self.client.on('close', () => {
            if (self.state >= states.DISCONNECTING) return
            self.remote.closed = true
            self.loginfo('client dropped connection', log_data)
            self.fail()
        })

        self.client.on('error', (err) => {
            if (self.state >= states.DISCONNECTING) return
            self.loginfo(`client connection error: ${err}`, log_data)
            self.fail()
        })

        self.client.on('timeout', () => {
            // FIN has sent, when timeout just destroy socket
            if (self.state >= states.DISCONNECTED) {
                self.client.destroy()
                self.loginfo(`timeout, destroy socket (state:${self.state})`)
                return
            }
            if (self.state >= states.DISCONNECTING) return
            self.respond(421, 'timeout', () => {
                self.fail('client connection timed out', log_data)
            })
        })

        self.client.on('data', (data) => {
            self.process_data(data)
        })

        // SMTPS pre-parser state: proxy means the PROXY line was already consumed;
        // peer_allowed means a trusted PROXY peer sent direct TLS instead.
        const smtps = self.client.haraka_smtps
        if (smtps?.proxy) {
            self.proxy.allowed = true
            return
        }

        if (smtps?.peer_allowed) {
            plugins.run_hooks('connect_init', self)
            return
        }

        if (net_utils.is_haproxy_allowed(self.remote.ip)) {
            self.proxy.allowed = true
            // Wait for PROXY command
            self.proxy.timer = setTimeout(() => {
                self.respond(421, 'PROXY timeout', () => {
                    self.disconnect()
                })
            }, 30 * 1000)
        } else {
            plugins.run_hooks('connect_init', self)
        }
    }
    setTLS(obj) {
        this.set('hello', 'host', undefined)
        this.set('tls', 'enabled', true)
        for (const t of ['cipher', 'verified', 'verifyError', 'peerCertificate']) {
            if (obj[t] === undefined) continue
            this.set('tls', t, obj[t])
        }
        // prior to 2017-07, authorized and verified were both used. Verified
        // seems to be the more common and has the property updated in the
        // tls object. However, authorized has been up-to-date in the notes. Store
        // in both, for backwards compatibility.
        this.notes.tls = {
            authorized: obj.verified, // legacy name
            authorizationError: obj.verifyError,
            cipher: obj.cipher,
            peerCertificate: obj.peerCertificate,
        }
    }
    set(prop_str, val) {
        if (arguments.length === 3) {
            prop_str = `${arguments[0]}.${arguments[1]}`
            val = arguments[2]
        }

        const path_parts = prop_str.split('.')
        let loc = this
        for (let i = 0; i < path_parts.length; i++) {
            const part = path_parts[i]
            if (part === '__proto__' || part === 'constructor') continue

            // while another part remains
            if (i < path_parts.length - 1) {
                if (loc[part] === undefined) loc[part] = {} // initialize
                loc = loc[part] // descend
                continue
            }

            // last part, so assign the value
            loc[part] = val
        }

        // Set is_private, is_local automatically when remote.ip is set
        if (prop_str === 'remote.ip') {
            this.set('remote.is_local', net_utils.is_local_ip(this.remote.ip))
            if (this.remote.is_local) {
                this.set('remote.is_private', true)
            } else {
                this.set('remote.is_private', net_utils.is_private_ip(this.remote.ip))
            }
        }
    }
    get(prop_str) {
        return prop_str.split('.').reduce((prev, curr) => {
            return prev ? prev[curr] : undefined
        }, this)
    }
    set relaying(val) {
        if (this.transaction) {
            this.transaction._relaying = val
        } else {
            this._relaying = val
        }
    }
    get relaying() {
        if (this.transaction && '_relaying' in this.transaction) return this.transaction._relaying
        return this._relaying
    }
    process_line(line) {
        if (this.state >= states.DISCONNECTING) {
            if (logger.would_log(logger.LOGPROTOCOL)) {
                this.logprotocol(`C: (after-disconnect): ${this.current_line}`, {
                    state: this.state,
                })
            }
            this.loginfo(`data after disconnect from ${this.remote.ip}`)
            return
        }

        if (this.state === states.DATA) {
            if (logger.would_log(logger.LOGDATA)) {
                this.logdata(`C: ${line}`)
            }
            this.accumulate_data(line)
            return
        }

        this.current_line = line.toString(this.encoding).replace(/\r?\n/, '')
        if (logger.would_log(logger.LOGPROTOCOL)) {
            this.logprotocol(`C: ${this.current_line}`, { state: this.state })
        }

        // Check for non-ASCII characters
        /* eslint no-control-regex: 0 */
        if (/[^\x00-\x7F]/.test(this.current_line)) {
            // See if this is a TLS handshake
            const buf = Buffer.from(this.current_line.slice(0, 3), 'binary')
            if (
                buf[0] === 0x16 &&
                buf[1] === 0x03 &&
                (buf[2] === 0x00 || buf[2] === 0x01) // SSLv3/TLS1.x format
            ) {
                // Nuke the current input buffer to prevent processing further input
                this.current_data = null
                this.respond(501, 'SSL attempted over a non-SSL socket')
                this.disconnect()
                return
            } else if (this.hello.verb == 'HELO') {
                return this.respond(501, 'Syntax error (8-bit characters not allowed)')
            }
        }

        if (this.state === states.CMD) {
            this.state = states.PAUSE_SMTP
            const matches = /^([^ ]*)( +(.*))?$/.exec(this.current_line)
            if (!matches) {
                return plugins.run_hooks('unrecognized_command', this, [this.current_line])
            }
            const cmd = matches[1]
            const method = `cmd_${cmd.toLowerCase()}`
            const remaining = matches[3] || ''
            if (this[method]) {
                try {
                    this[method](remaining)
                } catch (err) {
                    this.logerror(`${method} failed:`, err)
                    this.respond(421, 'Internal Server Error', () => {
                        this.disconnect()
                    })
                }
            } else {
                // unrecognized command
                plugins.run_hooks('unrecognized_command', this, [cmd, remaining])
            }
        } else if (this.state === states.LOOP) {
            // Allow QUIT
            if (this.current_line.toUpperCase() === 'QUIT') {
                this.state = states.PAUSE_SMTP
                this.cmd_quit()
            } else {
                this.respond(this.loop_code, this.loop_msg)
            }
        } else if (this.state === states.PAUSE_SMTP) {
            // Do nothing
        } else {
            throw new Error(`unknown state ${this.state}`)
        }
    }
    process_data(data) {
        if (this.state >= states.DISCONNECTING) {
            this.loginfo(`data after disconnect from ${this.remote.ip}`)
            return
        }

        if (!this.current_data || !this.current_data.length) {
            this.current_data = data
        } else {
            // Data left over in buffer
            const buf = Buffer.concat([this.current_data, data], this.current_data.length + data.length)
            this.current_data = buf
        }

        this._process_data()

        // Checked after processing so it also covers the read that carries the end-of-DATA
        // terminator and a pipelined command together. A compliant client sends nothing in
        // the soft-paused post-DATA window; a flood there is a pipelining attack.
        if (this.awaiting_data_reply && this.current_data?.length > MAX_POST_DATA_BYTES) {
            this.loginfo(`dropping ${this.current_data.length}B pipelined after DATA from ${this.remote.ip}`)
            this.current_data = null
            this.disconnect()
        }
    }
    _process_data() {
        // We *must* detect disconnected connections here as the state
        // only transitions to states.CMD in the respond function below.
        // Otherwise if multiple commands are pipelined and then the
        // connection is dropped; we'll end up in the function forever.
        if (this.state >= states.DISCONNECTING) return

        let maxlength
        if (this.state === states.PAUSE_DATA || this.state === states.DATA) {
            maxlength = cfg.max.data_line_length
        } else {
            maxlength = cfg.max.line_length
        }

        let offset
        while (this.current_data && (offset = utils.indexOfLF(this.current_data, maxlength)) !== -1) {
            if (this.state === states.PAUSE_DATA) {
                return
            }
            let this_line = this.current_data.slice(0, offset + 1)
            // Hack: bypass this code to allow HAProxy's PROXY extension
            const proxyStart = this.proxy.allowed && /^PROXY /.test(this_line)
            if (this.state === states.PAUSE && proxyStart) {
                this._clear_proxy_timer()
                this.state = states.CMD
                this.current_data = this.current_data.slice(this_line.length)
                this.process_line(this_line)
            }
            // Detect early_talker but allow PIPELINING extension (ESMTP)
            else if ((this.state === states.PAUSE || this.state === states.PAUSE_SMTP) && !this.esmtp) {
                // Allow EHLO/HELO to be pipelined with PROXY
                if (this.proxy.allowed && /^(?:EH|HE)LO /i.test(this_line)) return
                if (!this.early_talker) {
                    this_line = this_line.toString().replace(/\r?\n/, '')
                    this.logdebug('[early_talker]', {
                        state: this.state,
                        esmtp: this.esmtp,
                        line: this_line,
                    })
                }
                this.early_talker = true
                setImmediate(() => {
                    this._process_data()
                })
                break
            } else if ((this.state === states.PAUSE || this.state === states.PAUSE_SMTP) && this.esmtp) {
                let valid = true
                const cmd = this_line.toString('ascii').slice(0, 4).toUpperCase()
                switch (cmd) {
                    case 'RSET':
                    case 'MAIL':
                    case 'SEND':
                    case 'SOML':
                    case 'SAML':
                    case 'RCPT':
                        // These can be anywhere in the group
                        break
                    default:
                        // Anything else *MUST* be the last command in the group
                        if (this_line.length !== this.current_data.length) {
                            valid = false
                        }
                        break
                }
                if (valid) {
                    // Valid PIPELINING
                    // We *don't want to process this yet otherwise the
                    // current_data buffer will be lost.  The respond()
                    // function will call this function again once it
                    // has reset the state back to states.CMD and this
                    // ensures that we only process one command at a
                    // time.
                    this.pipelining = true
                    this.logdebug(`pipeline: ${this_line}`)
                } else {
                    // Invalid pipeline sequence
                    // Treat this as early talker
                    if (!this.early_talker) {
                        this.logdebug('[early_talker]', {
                            state: this.state,
                            esmtp: this.esmtp,
                            line: this_line,
                        })
                    }
                    this.early_talker = true
                    setImmediate(() => {
                        this._process_data()
                    })
                }
                break
            } else {
                this.current_data = this.current_data.slice(this_line.length)
                this.process_line(this_line)
            }
        }

        // Once end-of-DATA is seen the message is complete, so bytes still buffered are
        // pipelined commands, not body. Measuring them against data_line_length would
        // wrap them and flag the finished transaction; respond() judges them instead.
        if (
            this.current_data &&
            !this.awaiting_data_reply &&
            this.current_data.length > maxlength &&
            utils.indexOfLF(this.current_data, maxlength) === -1
        ) {
            if (this.state !== states.DATA && this.state !== states.PAUSE_DATA) {
                // In command mode, reject:
                this.client.pause()
                this.current_data = null
                return this.respond(521, 'Command line too long', () => {
                    this.disconnect()
                })
            } else {
                this.loginfo(`DATA line length (${this.current_data.length}) exceeds limit of ${maxlength} bytes`)
                this.transaction.notes.data_line_length_exceeded = true
                const b = Buffer.concat(
                    [
                        this.current_data.slice(0, maxlength - 2),
                        Buffer.from('\r\n ', 'utf8'),
                        this.current_data.slice(maxlength - 2),
                    ],
                    this.current_data.length + 3,
                )
                this.current_data = b
                return this._process_data()
            }
        }
    }
    respond(code, msg, func) {
        let uuid = ''
        let messages

        if (this.state === states.DISCONNECTED) {
            if (func) func()
            return
        }

        // First reply after end-of-DATA. The socket was only soft-paused, so current_data
        // holds whatever the client pipelined before it saw this reply. If our end-of-DATA
        // detection disagreed with the sender's, those bytes are attacker-authored, so none
        // of them run (GHSA-rp4q-8m6x-c43c) -- not even the RSET/MAIL FROM that RFC 2920
        // sec 3.1 would otherwise allow here. Each complete command is answered 503 below:
        // sec 3.1 has the client correlating responses by count, so silence desynchronises it.
        let refused_after_data = 0
        if (this.awaiting_data_reply) {
            this.awaiting_data_reply = false
            if (this.current_data?.length) {
                refused_after_data = count_complete_commands(this.current_data)
                this.lognotice('command pipelined after end of DATA - refused', {
                    remote: this.remote.ip,
                    buffered: this.current_data.length,
                    commands: refused_after_data,
                })
                this.current_data = null
            }
        }

        // Check to see if DSN object was passed in
        if (typeof msg === 'object' && msg.constructor.name === 'DSN') {
            // Override
            code = msg.code
            msg = msg.reply
        }

        if (!Array.isArray(msg)) {
            messages = msg.toString().split(/\n/)
        } else {
            messages = msg.slice()
        }
        messages = messages.filter((msg2) => /\S/.test(msg2))

        // Multiline AUTH PLAIN as in RFC-4954 page 8.
        if (code === 334 && !messages.length) {
            messages = [' ']
        }

        if (code >= 400) {
            this.last_reject = `${code} ${messages.join(' ')}`
            if (cfg.uuid.deny_chars) {
                uuid = (this.transaction || this).uuid
                if (cfg.uuid.deny_chars > 1) {
                    uuid = uuid.slice(0, cfg.uuid.deny_chars)
                }
            }
        }

        let mess
        let buf = ''
        const hostname = os.hostname().split('.').shift()
        const _uuid = uuid ? `[${uuid}@${hostname}] ` : ''

        while ((mess = messages.shift())) {
            const line = `${code}${messages.length ? '-' : ' '}${_uuid}${mess}`
            this.logprotocol(`S: ${line}`)
            buf = `${buf}${line}\r\n`
        }

        for (let i = 0; i < refused_after_data; i++) {
            const line = '503 Bad sequence of commands - pipelined after end of DATA'
            this.logprotocol(`S: ${line}`)
            buf = `${buf}${line}\r\n`
        }

        if (this.client.write === undefined) return buf // testing

        try {
            this.client.write(buf)
        } catch (err) {
            return this.fail(`Writing response: ${buf} failed: ${err}`)
        }

        // Store the last response
        this.last_response = buf

        // Don't change loop state
        if (this.state !== states.LOOP) {
            this.state = states.CMD
        }

        // Run optional closure before handling and further commands
        if (func) func()

        // Process any buffered commands (PIPELINING). Deferred so that a long
        // pipeline does not recurse
        setImmediate(() => this._process_data())
    }
    fail(err, err_data) {
        if (err) this.logwarn(err, err_data)
        this.hooks_to_run = []
        this.disconnect()
    }
    disconnect() {
        if (this.state >= states.DISCONNECTING) return
        this.state = states.DISCONNECTING
        this.current_data = null // don't process any more data we have already received
        // the client can disconnect before sending its PROXY header
        this._clear_proxy_timer()
        this.reset_transaction(() => {
            plugins.run_hooks('disconnect', this)
        })
    }
    disconnect_respond() {
        const logdetail = {
            ip: this.remote.ip,
            rdns: this.remote.host ? this.remote.host : '',
            helo: this.hello.host ? this.hello.host : '',
            relay: this.relaying ? 'Y' : 'N',
            early: this.early_talker ? 'Y' : 'N',
            esmtp: this.esmtp ? 'Y' : 'N',
            tls: this.tls.enabled ? 'Y' : 'N',
            pipe: this.pipelining ? 'Y' : 'N',
            errors: this.errors,
            txns: this.tran_count,
            rcpts: `${this.rcpt_count.accept}/${this.rcpt_count.tempfail}/${this.rcpt_count.reject}`,
            msgs: `${this.msg_count.accept}/${this.msg_count.tempfail}/${this.msg_count.reject}`,
            bytes: this.totalbytes,
            lr: this.last_reject ? this.last_reject : '',
            time: (Date.now() - this.start_time) / 1000,
        }

        this.results.add(
            { name: 'disconnect' },
            {
                duration: (Date.now() - this.start_time) / 1000,
            },
        )
        this.lognotice('disconnect', logdetail)
        this.state = states.DISCONNECTED
        this.client.end()
    }
    get_capabilities() {
        return []
    }
    tran_uuid() {
        this.tran_count++
        return `${this.uuid}.${this.tran_count}`
    }
    reset_transaction(cb) {
        this.results.add(
            { name: 'reset' },
            {
                duration: (Date.now() - this.start_time) / 1000,
            },
        )
        if (this.transaction && this.transaction.resetting === false) {
            // Pause connection to allow the hook to complete
            this.pause()
            this.transaction.resetting = true
            plugins.run_hooks('reset_transaction', this, cb)
        } else {
            this.transaction = null
            if (cb) cb()
        }
    }
    reset_transaction_respond(retval, msg, cb) {
        if (this.transaction) {
            this.transaction.message_stream.destroy()
            this.transaction = null
        }
        if (cb) cb()
        // Allow the connection to continue
        this.resume()
    }
    init_transaction(cb) {
        this.reset_transaction(() => {
            this.transaction = trans.createTransaction(this.tran_uuid(), cfg)
            // Catch any errors from the message_stream
            this.transaction.message_stream.on('error', (err) => {
                this.logcrit(`message_stream error: ${err.message}`)
                this.respond('421', 'Internal Server Error', () => {
                    this.disconnect()
                })
            })
            this.transaction.results = new ResultStore(this)
            if (cb) cb()
        })
    }
    loop_respond(code, msg) {
        if (this.state >= states.DISCONNECTING) return
        this.state = states.LOOP
        this.loop_code = code
        this.loop_msg = msg
        this.respond(code, msg)
    }
    pause() {
        if (this.state >= states.DISCONNECTING) return
        this.client.pause()
        if (this.state !== states.PAUSE_DATA) this.prev_state = this.state
        this.state = states.PAUSE_DATA
    }
    resume() {
        if (this.state >= states.DISCONNECTING) return
        this.client.resume()
        if (this.prev_state && this.state === states.PAUSE_DATA) {
            this.state = this.prev_state
        }
        this.prev_state = null
        setImmediate(() => this._process_data())
    }
    /////////////////////////////////////////////////////////////////////////////
    // SMTP Responses
    connect_init_respond() {
        this.logdebug('running connect_init_respond')
        plugins.run_hooks('lookup_rdns', this)
    }
    lookup_rdns_respond(retval, msg) {
        switch (retval) {
            case constants.ok:
                this.set('remote', 'host', utils.sanitize(msg) || 'Unknown')
                this.set('remote', 'info', this.remote.info || this.remote.host)
                plugins.run_hooks('connect', this)
                break
            case constants.deny:
                this.loop_respond(554, msg || 'rDNS Lookup Failed')
                break
            case constants.denydisconnect:
            case constants.disconnect:
                this.respond(554, msg || 'rDNS Lookup Failed', () => {
                    this.disconnect()
                })
                break
            case constants.denysoft:
                this.loop_respond(421, msg || 'rDNS Temporary Failure')
                break
            case constants.denysoftdisconnect:
                this.respond(421, msg || 'rDNS Temporary Failure', () => {
                    this.disconnect()
                })
                break
            default:
                // BUG: dns.reverse throws on invalid input (and sometimes valid
                // input nodejs/node#47847). Also throws when empty results
                try {
                    dns.reverse(this.remote.ip, (err, domains) => {
                        this.rdns_response(err, domains)
                    })
                } catch (err) {
                    this.rdns_response(err, [])
                }
        }
    }
    rdns_response(err, domains) {
        if (err) {
            switch (err.code) {
                case dns.NXDOMAIN:
                case dns.NOTFOUND:
                    this.set('remote', 'host', 'NXDOMAIN')
                    break
                default:
                    this.set('remote', 'host', 'DNSERROR')
                    break
            }
        } else {
            this.set('remote', 'host', utils.sanitize(domains[0]) || 'Unknown')
            this.results.add({ name: 'remote' }, this.remote)
        }
        this.set('remote', 'info', this.remote.info || this.remote.host)
        plugins.run_hooks('connect', this)
    }
    unrecognized_command_respond(retval, msg) {
        switch (retval) {
            case constants.ok:
                // response already sent, cool...
                break
            case constants.next_hook:
                plugins.run_hooks(msg, this)
                break
            case constants.deny:
                this.respond(500, msg || 'Unrecognized command')
                break
            case constants.denydisconnect:
            case constants.denysoftdisconnect:
                this.respond(retval === constants.denydisconnect ? 521 : 421, msg || 'Unrecognized command', () => {
                    this.disconnect()
                })
                break
            default:
                this.errors++
                this.respond(500, msg || 'Unrecognized command')
        }
    }
    connect_respond(retval, msg) {
        // RFC 5321 Section 4.3.2 states that the only valid SMTP codes here are:
        // 220 = Service ready
        // 554 = Transaction failed (no SMTP service here)
        // 421 = Service shutting down and closing transmission channel
        switch (retval) {
            case constants.deny:
                this.loop_respond(554, msg || 'Your mail is not welcome here')
                break
            case constants.denydisconnect:
            case constants.disconnect:
                this.respond(554, msg || 'Your mail is not welcome here', () => {
                    this.disconnect()
                })
                break
            case constants.denysoft:
                this.loop_respond(421, msg || 'Come back later')
                break
            case constants.denysoftdisconnect:
                this.respond(421, msg || 'Come back later', () => {
                    this.disconnect()
                })
                break
            default: {
                let greeting
                if (cfg.message.greeting?.length) {
                    // RFC5321 section 4.2
                    // Hostname/domain should appear after the 220
                    greeting = [...cfg.message.greeting]
                    greeting[0] = `${this.local.host} ESMTP ${greeting[0]}`
                    if (cfg.uuid.banner_chars) {
                        greeting[0] += ` (${this.uuid.slice(0, cfg.uuid.banner_chars)})`
                    }
                } else {
                    greeting = `${this.local.host} ESMTP ${this.local.info} ready`
                    if (cfg.uuid.banner_chars) {
                        greeting += ` (${this.uuid.slice(0, cfg.uuid.banner_chars)})`
                    }
                }
                this.respond(220, msg || greeting)
            }
        }
    }
    get_remote(prop) {
        switch (this.remote[prop]) {
            case 'NXDOMAIN':
            case 'DNSERROR':
            case '':
            case undefined:
            case null:
                return `[${this.remote.ip}]`
            default:
                return `${this.remote[prop]} [${this.remote.ip}]`
        }
    }
    helo_respond(retval, msg) {
        switch (retval) {
            case constants.deny:
                this.respond(550, msg || 'HELO denied', () => {
                    this.set('hello', 'verb', null)
                    this.set('hello', 'host', null)
                })
                break
            case constants.denydisconnect:
                this.respond(550, msg || 'HELO denied', () => {
                    this.disconnect()
                })
                break
            case constants.denysoft:
                this.respond(450, msg || 'HELO denied', () => {
                    this.set('hello', 'verb', null)
                    this.set('hello', 'host', null)
                })
                break
            case constants.denysoftdisconnect:
                this.respond(450, msg || 'HELO denied', () => {
                    this.disconnect()
                })
                break
            default:
                // RFC5321 section 4.1.1.1
                // Hostname/domain should appear after 250
                this.respond(250, `${this.local.host} Hello ${this.get_remote('host')}, ${cfg.message.helo}`)
        }
    }
    ehlo_respond(retval, msg) {
        switch (retval) {
            case constants.deny:
                this.respond(550, msg || 'EHLO denied', () => {
                    this.set('hello', 'verb', null)
                    this.set('hello', 'host', null)
                })
                break
            case constants.denydisconnect:
                this.respond(550, msg || 'EHLO denied', () => {
                    this.disconnect()
                })
                break
            case constants.denysoft:
                this.respond(450, msg || 'EHLO denied', () => {
                    this.set('hello', 'verb', null)
                    this.set('hello', 'host', null)
                })
                break
            case constants.denysoftdisconnect:
                this.respond(450, msg || 'EHLO denied', () => {
                    this.disconnect()
                })
                break
            default: {
                // RFC5321 section 4.1.1.1
                // Hostname/domain should appear after 250

                const response = [
                    `${this.local.host} Hello ${this.get_remote('host')}, ${cfg.message.helo}`,
                    'PIPELINING',
                    '8BITMIME',
                ]

                if (cfg.main.smtputf8) response.push('SMTPUTF8')

                response.push(`SIZE ${cfg.max.bytes}`)

                this.capabilities = response

                plugins.run_hooks('capabilities', this)
                this.esmtp = true
            }
        }
    }
    capabilities_respond() {
        this.respond(250, this.capabilities)
    }
    quit_respond(retval, msg) {
        this.respond(221, msg || `${this.local.host} ${cfg.message.close}`, () => {
            this.disconnect()
        })
    }
    vrfy_respond(retval, msg) {
        switch (retval) {
            case constants.deny:
                this.respond(550, msg || 'Access Denied', () => {
                    this.reset_transaction()
                })
                break
            case constants.denydisconnect:
                this.respond(550, msg || 'Access Denied', () => {
                    this.disconnect()
                })
                break
            case constants.denysoft:
                this.respond(450, msg || 'Lookup Failed', () => {
                    this.reset_transaction()
                })
                break
            case constants.denysoftdisconnect:
                this.respond(450, msg || 'Lookup Failed', () => {
                    this.disconnect()
                })
                break
            case constants.ok:
                this.respond(250, msg || 'User OK')
                break
            default:
                this.respond(252, "Just try sending a mail and we'll see how it turns out...")
        }
    }
    noop_respond(retval, msg) {
        switch (retval) {
            case constants.deny:
                this.respond(500, msg || 'Stop wasting my time')
                break
            case constants.denydisconnect:
                this.respond(500, msg || 'Stop wasting my time', () => {
                    this.disconnect()
                })
                break
            default:
                this.respond(250, 'OK')
        }
    }
    rset_respond() {
        this.respond(250, 'OK', () => {
            this.reset_transaction()
        })
    }
    mail_respond(retval, msg) {
        if (!this.transaction) {
            this.logerror('mail_respond found no transaction!')
            return
        }
        const sender = this.transaction.mail_from
        const dmsg = `sender ${sender.format()}`
        this.lognotice(dmsg, {
            code: constants.translate(retval),
            msg: msg || '',
        })

        const store_results = (action) => {
            let addr = sender.format()
            if (addr.length > 2) {
                // all but null sender
                addr = addr.slice(1, -1) // trim off < >
            }
            this.transaction.results.add(
                { name: 'mail_from' },
                {
                    action,
                    code: constants.translate(retval),
                    address: addr,
                },
            )
        }

        switch (retval) {
            case constants.deny:
                this.respond(550, msg || `${dmsg} denied`, () => {
                    store_results('reject')
                    this.reset_transaction()
                })
                break
            case constants.denydisconnect:
                this.respond(550, msg || `${dmsg} denied`, () => {
                    store_results('reject')
                    this.disconnect()
                })
                break
            case constants.denysoft:
                this.respond(450, msg || `${dmsg} denied`, () => {
                    store_results('tempfail')
                    this.reset_transaction()
                })
                break
            case constants.denysoftdisconnect:
                this.respond(450, msg || `${dmsg} denied`, () => {
                    store_results('tempfail')
                    this.disconnect()
                })
                break
            default:
                store_results('accept')
                this.respond(250, msg || `${dmsg} OK`)
        }
    }
    rcpt_incr(rcpt, action, msg, retval) {
        this.transaction.rcpt_count[action]++
        this.rcpt_count[action]++

        const addr = rcpt.format()
        const recipient = {
            address: addr.slice(1, -1),
            action,
        }

        if (msg && action !== 'accept') {
            if (typeof msg === 'object' && msg.constructor.name === 'DSN') {
                recipient.msg = msg.reply
                recipient.code = msg.code
            } else {
                recipient.msg = msg
                recipient.code = constants.translate(retval)
            }
        }

        this.transaction.results.push({ name: 'rcpt_to' }, { recipient })
    }
    rcpt_ok_respond(retval, msg) {
        if (!this.transaction) {
            this.results.add(this, {
                err: 'rcpt_ok_respond found no transaction',
            })
            return
        }
        if (!msg) msg = this.last_rcpt_msg
        const rcpt = this.transaction.rcpt_to[this.transaction.rcpt_to.length - 1]
        const dmsg = `recipient ${rcpt.format()}`
        // Log OK instead of CONT as this hook only runs if hook_rcpt returns OK
        this.lognotice(dmsg, {
            code: constants.translate(retval === constants.cont ? constants.ok : retval),
            msg: msg || '',
            sender: this.transaction.mail_from.address,
        })
        switch (retval) {
            case constants.deny:
                this.respond(550, msg || `${dmsg} denied`, () => {
                    this.rcpt_incr(rcpt, 'reject', msg, retval)
                    this.transaction.rcpt_to.pop()
                })
                break
            case constants.denydisconnect:
                this.respond(550, msg || `${dmsg} denied`, () => {
                    this.rcpt_incr(rcpt, 'reject', msg, retval)
                    this.disconnect()
                })
                break
            case constants.denysoft:
                this.respond(450, msg || `${dmsg} denied`, () => {
                    this.rcpt_incr(rcpt, 'tempfail', msg, retval)
                    this.transaction.rcpt_to.pop()
                })
                break
            case constants.denysoftdisconnect:
                this.respond(450, msg || `${dmsg} denied`, () => {
                    this.rcpt_incr(rcpt, 'tempfail', msg, retval)
                    this.disconnect()
                })
                break
            default:
                this.respond(250, msg || `${dmsg} OK`, () => {
                    this.rcpt_incr(rcpt, 'accept', msg, retval)
                })
        }
    }
    rcpt_respond(retval, msg) {
        if (retval === constants.cont && this.relaying) {
            retval = constants.ok
        }

        if (!this.transaction) {
            this.results.add(this, {
                err: 'rcpt_respond found no transaction',
            })
            return
        }
        const rcpt = this.transaction.rcpt_to[this.transaction.rcpt_to.length - 1]
        const dmsg = `recipient ${rcpt.format()}`
        if (retval !== constants.ok) {
            this.lognotice(dmsg, {
                code: constants.translate(retval === constants.cont ? constants.ok : retval),
                msg: msg || '',
                sender: this.transaction.mail_from.address,
            })
        }
        switch (retval) {
            case constants.deny:
                this.respond(550, msg || `${dmsg} denied`, () => {
                    this.rcpt_incr(rcpt, 'reject', msg, retval)
                    this.transaction.rcpt_to.pop()
                })
                break
            case constants.denydisconnect:
                this.respond(550, msg || `${dmsg} denied`, () => {
                    this.rcpt_incr(rcpt, 'reject', msg, retval)
                    this.disconnect()
                })
                break
            case constants.denysoft:
                this.respond(450, msg || `${dmsg} denied`, () => {
                    this.rcpt_incr(rcpt, 'tempfail', msg, retval)
                    this.transaction.rcpt_to.pop()
                })
                break
            case constants.denysoftdisconnect:
                this.respond(450, msg || `${dmsg} denied`, () => {
                    this.rcpt_incr(rcpt, 'tempfail', msg, retval)
                    this.disconnect()
                })
                break
            case constants.ok:
                // Store any msg for rcpt_ok
                this.last_rcpt_msg = msg
                plugins.run_hooks('rcpt_ok', this, rcpt)
                break
            default: {
                if (retval !== constants.cont) {
                    this.logalert('No plugin determined if relaying was allowed')
                }
                const rej_msg = `I cannot deliver mail for ${rcpt.format()}`
                this.respond(550, rej_msg, () => {
                    this.rcpt_incr(rcpt, 'reject', rej_msg, retval)
                    this.transaction.rcpt_to.pop()
                })
            }
        }
    }
    /////////////////////////////////////////////////////////////////////////////
    // HAProxy support

    _clear_proxy_timer() {
        if (this.proxy.timer) {
            clearTimeout(this.proxy.timer)
            this.proxy.timer = null
        }
    }

    apply_proxy(proxy) {
        this._clear_proxy_timer()

        const { proto, src_ip, src_port, dst_ip, dst_port } = proxy
        const proxy_ip = proxy.proxy_ip || this.remote.ip

        // Apply changes
        this.loginfo('HAProxy', {
            proto,
            src_ip: `${new net_utils.Endpoint({ host: src_ip, port: src_port })}`,
            dst_ip: `${new net_utils.Endpoint({ host: dst_ip, port: dst_port })}`,
        })

        this.notes.proxy = {
            type: 'haproxy',
            proto,
            src_ip,
            src_port,
            dst_ip,
            dst_port,
            proxy_ip,
        }

        this.reset_transaction(() => {
            this.set('proxy.ip', proxy_ip)
            this.set('proxy.type', 'haproxy')
            this.relaying = false
            this.set('local.ip', dst_ip)
            this.set('local.port', parseInt(dst_port, 10))
            this.set('remote.ip', src_ip)
            this.set('remote.port', parseInt(src_port, 10))
            this.set('remote.host', null)
            this.set('hello.host', null)
            this.results.add({ name: 'local' }, this.local)
            this.results.add({ name: 'remote' }, this.remote)
            plugins.run_hooks('connect_init', this)
        })
    }

    cmd_proxy(line) {
        if (!this.proxy.allowed) {
            this.respond(421, `PROXY not allowed from ${this.remote.ip}`)
            return this.disconnect()
        }

        const proxy = net_utils.parse_proxy_line(line)
        if (!proxy) {
            this.respond(421, 'Invalid PROXY format')
            return this.disconnect()
        }

        this.apply_proxy(proxy)
    }
    /////////////////////////////////////////////////////////////////////////////
    // SMTP Commands

    cmd_internalcmd(line) {
        if (!this.remote.is_local) {
            return this.respond(501, 'INTERNALCMD not allowed remotely')
        }
        const results = String(line).split(/ +/)
        if (/key:/.test(results[0])) {
            const internal_key = config.get('internalcmd_key')
            if (results[0] != `key:${internal_key}`) {
                return this.respond(501, 'Invalid internalcmd_key - check config')
            }
            results.shift()
        } else if (config.get('internalcmd_key')) {
            return this.respond(501, 'Missing internalcmd_key - check config')
        }

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

        require('./server').sendToMaster(command, results)
        return this.respond(250, 'Command sent for execution. Check Haraka logs for results.')
    }
    cmd_helo(line) {
        const results = String(line).split(/ +/)
        const host = results[0]
        if (!host) {
            return this.respond(501, 'HELO requires domain/address - see RFC-2821 4.1.1.1')
        }
        // RFC 5321 §4.1.1.1: the domain/address-literal cannot contain
        // control characters. process_line() only strips the first \r?\n,
        // so a bare \r could otherwise survive into hello.host and the
        // generated Received: header / logs (header injection).
        if (/[\x00-\x1f\x7f]/.test(host)) {
            return this.respond(501, 'HELO syntax error - see RFC-2821 4.1.1.1')
        }

        this.reset_transaction(() => {
            this.set('hello', 'verb', 'HELO')
            this.set('hello', 'host', host)
            this.results.add({ name: 'helo' }, this.hello)
            plugins.run_hooks('helo', this, host)
        })
    }
    cmd_ehlo(line) {
        const results = String(line).split(/ +/)
        const host = results[0]
        if (!host) {
            return this.respond(501, 'EHLO requires domain/address - see RFC-2821 4.1.1.1')
        }
        // RFC 5321 §4.1.1.1: reject control chars (see cmd_helo).
        if (/[\x00-\x1f\x7f]/.test(host)) {
            return this.respond(501, 'EHLO syntax error - see RFC-2821 4.1.1.1')
        }

        this.reset_transaction(() => {
            this.set('hello', 'verb', 'EHLO')
            this.set('hello', 'host', host)
            this.results.add({ name: 'helo' }, this.hello)
            plugins.run_hooks('ehlo', this, host)
        })
    }
    cmd_quit(args) {
        // RFC 5321 Section 4.3.2
        // QUIT does not accept arguments
        if (args) {
            return this.respond(501, 'Syntax error')
        }
        plugins.run_hooks('quit', this)
    }
    cmd_rset(args) {
        // RFC 5321 Section 4.3.2
        // RSET does not accept arguments
        if (args) {
            return this.respond(501, 'Syntax error')
        }
        plugins.run_hooks('rset', this)
    }
    cmd_vrfy() {
        // only supported via plugins
        plugins.run_hooks('vrfy', this)
    }
    cmd_noop() {
        plugins.run_hooks('noop', this)
    }
    cmd_help() {
        this.respond(250, 'Not implemented')
    }
    cmd_mail(line) {
        if (!this.hello.host) {
            this.errors++
            return this.respond(503, 'Use EHLO/HELO before MAIL')
        }
        // Require authentication on ports 587 & 465
        if (!this.relaying && [587, 465].includes(this.local.port)) {
            this.errors++
            return this.respond(550, 'Authentication required')
        }

        let results
        try {
            results = rfc1869.parse('mail', line, !this.relaying && cfg.main.strict_rfc1869)
        } catch (err) {
            this.errors++
            if (err.stack) {
                this.lognotice(err.stack.split(/\n/)[0])
            } else {
                this.logerror(err)
            }
            // Explicitly handle out-of-disk space errors
            if (err.code === 'ENOSPC') {
                return this.respond(452, 'Internal Server Error')
            } else {
                return this.respond(501, ['Command parsing failed', err])
            }
        }

        let from
        const from_raw = results.shift()
        try {
            from = new Address(from_raw, { postel: cfg.main.postel })
        } catch (err) {
            const msg = `Invalid MAIL FROM address ${utils.sanitize(from_raw)}: ${err.message}`
            this.lognotice(msg)
            return this.respond(501, msg)
        }

        // Get rest of key=value pairs
        const params = {}
        for (const param of results) {
            const kv = param.match(/^([^=]+)(?:=(.+))?$/)
            if (kv) params[kv[1].toUpperCase()] = kv[2] || null
        }

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

        // Handle SIZE extension
        if (params?.SIZE && params.SIZE > 0) {
            if (cfg.max.bytes > 0 && params.SIZE > cfg.max.bytes) {
                return this.respond(550, 'Message too big!')
            }
        }

        this.init_transaction(() => {
            this.transaction.mail_from = from
            if (this.hello.verb == 'HELO') {
                this.transaction.encoding = 'binary'
                this.encoding = 'binary'
            }
            plugins.run_hooks('mail', this, [from, params])
        })
    }
    cmd_rcpt(line) {
        if (!this.transaction || !this.transaction.mail_from) {
            this.errors++
            return this.respond(503, 'Use MAIL before RCPT')
        }

        let results
        try {
            results = rfc1869.parse('rcpt', line, cfg.main.strict_rfc1869 && !this.relaying)
        } catch (err) {
            this.errors++
            if (err.stack) {
                this.lognotice(err.stack.split(/\n/)[0])
            } else {
                this.logerror(err)
            }
            // Explicitly handle out-of-disk space errors
            if (err.code === 'ENOSPC') {
                return this.respond(452, 'Internal Server Error')
            } else {
                return this.respond(501, ['Command parsing failed', err])
            }
        }

        let recip
        const recip_raw = results.shift()
        try {
            recip = new Address(recip_raw, { postel: cfg.main.postel })
        } catch (err) {
            const msg = `Invalid RCPT TO address ${utils.sanitize(recip_raw)}: ${err.message}`
            this.lognotice(msg)
            return this.respond(501, msg)
        }

        // Get rest of key=value pairs
        const params = {}
        for (const param of results) {
            const kv = param.match(/^([^=]+)(?:=(.+))?$/)
            if (kv) params[kv[1].toUpperCase()] = kv[2] || null
        }

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

        this.transaction.rcpt_to.push(recip)
        plugins.run_hooks('rcpt', this, [recip, params])
    }
    received_line() {
        let smtp = this.hello.verb === 'EHLO' ? 'ESMTP' : 'SMTP'
        // Implement RFC3848
        if (this.tls.enabled) smtp += 'S'
        if (this.authheader) smtp += 'A'

        let sslheader

        if (this.get('tls.cipher.version')) {
            // standardName appeared in Node.js v12.16 and v13.4
            // RFC 8314
            sslheader = `tls ${this.tls.cipher.standardName || this.tls.cipher.name}`
        }

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

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

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

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

        return received_header
    }
    auth_results(message) {
        // https://datatracker.ietf.org/doc/rfc7001/
        const has_tran = !!this.transaction?.notes

        // initialize connection note
        if (!this.notes.authentication_results) {
            this.notes.authentication_results = []
        }

        // initialize transaction note, if possible
        if (has_tran === true && !this.transaction.notes.authentication_results) {
            this.transaction.notes.authentication_results = []
        }

        // Strip CR/LF and other control chars: an attacker-influenced
        // value (e.g. a failed AUTH username, see auth_base) must not be
        // able to inject extra header lines into Authentication-Results.
        // The legitimate folding (;\r\n\t) is added by the join below.
        // if message, store it in the appropriate note
        if (message) {
            if (has_tran === true) {
                this.transaction.notes.authentication_results.push(utils.sanitize(message))
            } else {
                this.notes.authentication_results.push(utils.sanitize(message))
            }
        }

        // assemble the new header
        let header = [utils.sanitize(this.local.host), ...this.notes.authentication_results]
        if (has_tran === true) {
            header = [...header, ...this.transaction.notes.authentication_results]
        }
        if (header.length === 1) return '' // no results
        return header.join(';\r\n\t')
    }
    auth_results_clean() {
        // move any existing Auth-Res headers to Original-Auth-Res headers
        // http://tools.ietf.org/html/draft-kucherawy-original-authres-00.html
        const ars = this.transaction.header.get_all('Authentication-Results')
        if (ars.length === 0) return

        for (const element of ars) {
            this.transaction.add_header('Original-Authentication-Results', element)
        }
        this.transaction.remove_header('Authentication-Results')
        this.logdebug('Authentication-Results moved to Original-Authentication-Results')
    }
    cmd_data(args) {
        // RFC 5321 Section 4.3.2
        // DATA does not accept arguments
        if (args) {
            this.errors++
            return this.respond(501, 'Syntax error')
        }
        if (!this.transaction) {
            this.errors++
            return this.respond(503, 'MAIL required first')
        }
        if (!this.transaction.rcpt_to.length) {
            if (this.pipelining) {
                return this.respond(554, 'No valid recipients')
            }
            this.errors++
            return this.respond(503, 'RCPT required first')
        }

        if (cfg.headers.add_received) {
            this.accumulate_data(`Received: ${this.received_line()}\r\n`)
        }
        plugins.run_hooks('data', this)
    }
    data_respond(retval, msg) {
        let cont = 0
        switch (retval) {
            case constants.deny:
                this.respond(554, msg || 'Message denied', () => {
                    this.reset_transaction()
                })
                break
            case constants.denydisconnect:
                this.respond(554, msg || 'Message denied', () => {
                    this.disconnect()
                })
                break
            case constants.denysoft:
                this.respond(451, msg || 'Message denied', () => {
                    this.reset_transaction()
                })
                break
            case constants.denysoftdisconnect:
                this.respond(451, msg || 'Message denied', () => {
                    this.disconnect()
                })
                break
            default:
                cont = 1
        }
        if (!cont) return

        // We already checked for MAIL/RCPT in cmd_data
        this.respond(354, 'go ahead, make my day', () => {
            // OK... now we get the data
            this.state = states.DATA
            this.transaction.data_bytes = 0
        })
    }
    accumulate_data(line) {
        this.transaction.data_bytes += line.length

        // Look for .\r\n
        if (line.length === 3 && line[0] === 0x2e && line[1] === 0x0d && line[2] === 0x0a) {
            this.data_done()
            return
        }

        // Look for .\n
        if (line.length === 2 && line[0] === 0x2e && line[1] === 0x0a) {
            this.lognotice('Client sent bare line-feed - .\\n rather than .\\r\\n')
            this.respond(451, 'Bare line-feed; see https://haraka.github.io/barelf/', () => {
                this.disconnect()
            })
            return
        }

        // The line stream also splits on a bare LF, so <LF>.<CRLF> would end DATA here
        // while a CRLF-only peer forwards the same bytes as content (SMTP smuggling).
        if (line[line.length - 1] === 0x0a && line[line.length - 2] !== 0x0d) {
            this.lognotice('Client sent bare line-feed inside DATA')
            this.respond(451, 'Bare line-feed; see https://haraka.github.io/barelf/', () => {
                this.disconnect()
            })
            return
        }

        // Stop accumulating data as we're going to reject at dot.
        if (cfg.max.bytes && this.transaction.data_bytes > cfg.max.bytes) {
            return
        }

        if (this.transaction.mime_part_count >= cfg.max.mime_parts) {
            this.logcrit('Possible DoS attempt - too many MIME parts')
            this.respond(554, 'Transaction failed due to too many MIME parts', () => {
                this.disconnect()
            })
            return
        }

        this.transaction.add_data(line)
    }
    data_done() {
        // Soft pause: keep reading the socket so a command pipelined after the
        // terminator lands in current_data. A hard client.pause() would leave it
        // in the kernel queue, invisible to a buffer check. PAUSE_DATA still stops
        // it being processed until resume().
        if (this.state !== states.PAUSE_DATA) this.prev_state = this.state
        this.state = states.PAUSE_DATA
        this.awaiting_data_reply = true
        this.totalbytes += this.transaction.data_bytes

        // Check message size limit
        if (cfg.max.bytes && this.transaction.data_bytes > cfg.max.bytes) {
            this.lognotice(`Incoming message exceeded max size of ${cfg.max.bytes}`)
            return plugins.run_hooks('max_data_exceeded', this)
        }

        // Check max received headers count
        if (this.transaction.header.get_all('received').length > cfg.headers.max_received) {
            this.logerror('Incoming message had too many Received headers')
            this.respond(550, 'Too many received headers - possible mail loop', () => {
                this.reset_transaction()
            })
            return
        }

        // Warn if we hit the maximum parsed header lines limit
        if (this.transaction.header_lines.length >= cfg.headers.max_lines) {
            this.logwarn(`Incoming message reached maximum parsing limit of ${cfg.headers.max_lines} header lines`)
        }

        if (cfg.headers.clean_auth_results) {
            this.auth_results_clean() // rename old A-R headers
        }
        const ar_field = this.auth_results() // assemble new one
        if (ar_field) {
            this.transaction.add_header('Authentication-Results', ar_field)
        }

        this.transaction.end_data(() => {
            // As this will be called asynchronously,
            // make sure we still have a transaction.
            if (!this.transaction) return
            // Record the start time of this hook as we can't take too long
            // as the client will typically hang up after 2 to 3 minutes
            // despite the RFC mandating that 10 minutes should be allowed.
            this.transaction.data_post_start = Date.now()
            plugins.run_hooks('data_post', this)
        })
    }
    data_post_respond(retval, msg) {
        if (!this.transaction) return
        this.transaction.data_post_delay = (Date.now() - this.transaction.data_post_start) / 1000
        const mid = utils.sanitize(this.transaction.header.get('Message-ID') || '', { replacement: ' ' }).trim()
        this.lognotice('message', {
            mid,
            size: this.transaction.data_bytes,
            rcpts: `${this.transaction.rcpt_count.accept}/${this.transaction.rcpt_count.tempfail}/${this.transaction.rcpt_count.reject}`,
            delay: this.transaction.data_post_delay,
            code: constants.translate(retval),
            msg: msg || '',
        })
        const ar_field = this.auth_results() // assemble A-R header
        if (ar_field) {
            this.transaction.remove_header('Authentication-Results')
            this.transaction.add_leading_header('Authentication-Results', ar_field)
        }
        switch (retval) {
            case constants.deny:
                this.respond(550, msg || 'Message denied', () => {
                    this.msg_count.reject++
                    this.transaction.msg_status = 'rejected'
                    this.reset_transaction(() => this.resume())
                })
                break
            case constants.denydisconnect:
                this.respond(550, msg || 'Message denied', () => {
                    this.msg_count.reject++
                    this.transaction.msg_status = 'rejected'
                    this.disconnect()
                })
                break
            case constants.denysoft:
                this.respond(450, msg || 'Message denied temporarily', () => {
                    this.msg_count.tempfail++
                    this.transaction.msg_status = 'deferred'
                    this.reset_transaction(() => this.resume())
                })
                break
            case constants.denysoftdisconnect:
                this.respond(450, msg || 'Message denied temporarily', () => {
                    this.msg_count.tempfail++
                    this.transaction.msg_status = 'deferred'
                    this.disconnect()
                })
                break
            default:
                if (this.relaying) {
                    plugins.run_hooks('queue_outbound', this)
                } else {
                    plugins.run_hooks('queue', this)
                }
        }
    }
    max_data_exceeded_respond(retval) {
        // TODO: Maybe figure out what to do with other return codes
        this.respond(retval === constants.denysoft ? 450 : 550, 'Message too big!', () => {
            this.reset_transaction()
        })
    }
    queue_msg(retval, msg) {
        if (msg) {
            if (typeof msg === 'object' && msg.constructor.name === 'DSN') {
                return msg.reply
            }
            return msg
        }

        switch (retval) {
            case constants.ok:
                return 'Message Queued'
            case constants.deny:
            case constants.denydisconnect:
                return 'Message denied'
            case constants.denysoft:
            case constants.denysoftdisconnect:
                return 'Message denied temporarily'
            default:
                return ''
        }
    }
    store_queue_result(retval, msg) {
        const res_as = { name: 'queue' }
        switch (retval) {
            case constants.ok:
                this.transaction.results.add(res_as, { pass: msg })
                break
            case constants.deny:
            case constants.denydisconnect:
                this.transaction.results.add(res_as, { fail: msg })
                break
            case constants.denysoft:
            case constants.denysoftdisconnect:
                this.transaction.results.add(res_as, { fail: msg, soft: true })
                break
            case constants.cont:
                break
            default:
                this.transaction.results.add(res_as, { msg })
                break
        }
    }
    queue_outbound_respond(retval, msg) {
        if (this.remote.closed) return
        msg = this.queue_msg(retval, msg) || 'Message Queued'
        this.store_queue_result(retval, msg)
        msg = `${msg} (${this.transaction.uuid})`
        if (retval !== constants.ok) {
            this.lognotice('queue', {
                code: constants.translate(retval),
                msg,
            })
        }
        switch (retval) {
            case constants.ok:
                plugins.run_hooks('queue_ok', this, msg)
                break
            case constants.deny:
                this.respond(550, msg, () => {
                    this.msg_count.reject++
                    this.transaction.msg_status = 'rejected'
                    this.reset_transaction(() => this.resume())
                })
                break
            case constants.denydisconnect:
                this.respond(550, msg, () => {
                    this.msg_count.reject++
                    this.transaction.msg_status = 'rejected'
                    this.disconnect()
                })
                break
            case constants.denysoft:
                this.respond(450, msg, () => {
                    this.msg_count.tempfail++
                    this.transaction.msg_status = 'deferred'
                    this.reset_transaction(() => this.resume())
                })
                break
            case constants.denysoftdisconnect:
                this.respond(450, msg, () => {
                    this.msg_count.tempfail++
                    this.transaction.msg_status = 'deferred'
                    this.disconnect()
                })
                break
            default:
                outbound.send_trans_email(this.transaction, (retval2, msg2) => {
                    if (!msg2) msg2 = this.queue_msg(retval2, msg)
                    switch (retval2) {
                        case constants.ok:
                            if (!msg2) msg2 = this.queue_msg(retval2, msg2)
                            plugins.run_hooks('queue_ok', this, msg2)
                            break
                        case constants.deny:
                            if (!msg2) msg2 = this.queue_msg(retval2, msg2)
                            this.respond(550, msg2, () => {
                                this.msg_count.reject++
                                this.transaction.msg_status = 'rejected'
                                this.reset_transaction(() => {
                                    this.resume()
                                })
                            })
                            break
                        default:
                            this.logerror(`Unrecognized response from outbound layer: ${retval2} : ${msg2}`)
                            this.respond(550, msg2 || 'Internal Server Error', () => {
                                this.msg_count.reject++
                                this.transaction.msg_status = 'rejected'
                                this.reset_transaction(() => {
                                    this.resume()
                                })
                            })
                    }
                })
        }
    }
    queue_respond(retval, msg) {
        msg = this.queue_msg(retval, msg)
        this.store_queue_result(retval, msg)
        msg = `${msg} (${this.transaction.uuid})`

        if (retval !== constants.ok) {
            this.lognotice('queue', {
                code: constants.translate(retval),
                msg,
            })
        }
        switch (retval) {
            case constants.ok:
                plugins.run_hooks('queue_ok', this, msg)
                break
            case constants.deny:
                this.respond(550, msg, () => {
                    this.msg_count.reject++
                    this.transaction.msg_status = 'rejected'
                    this.reset_transaction(() => this.resume())
                })
                break
            case constants.denydisconnect:
                this.respond(550, msg, () => {
                    this.msg_count.reject++
                    this.transaction.msg_status = 'rejected'
                    this.disconnect()
                })
                break
            case constants.denysoft:
                this.respond(450, msg, () => {
                    this.msg_count.tempfail++
                    this.transaction.msg_status = 'deferred'
                    this.reset_transaction(() => this.resume())
                })
                break
            case constants.denysoftdisconnect:
                this.respond(450, msg, () => {
                    this.msg_count.tempfail++
                    this.transaction.msg_status = 'deferred'
                    this.disconnect()
                })
                break
            default:
                if (!msg) msg = 'Queuing declined or disabled, try later'
                this.respond(451, msg, () => {
                    this.msg_count.tempfail++
                    this.transaction.msg_status = 'deferred'
                    this.reset_transaction(() => this.resume())
                })
                break
        }
    }
    queue_ok_respond(retval, msg, params) {
        // This hook is common to both hook_queue and hook_queue_outbound
        // retval and msg are ignored in this hook so we always log OK
        this.lognotice('queue', {
            code: 'OK',
            msg: params || '',
        })

        this.respond(250, params, () => {
            this.msg_count.accept++
            if (this.transaction) this.transaction.msg_status = 'accepted'
            this.reset_transaction(() => this.resume())
        })
    }
}

exports.Connection = Connection

exports.cfg = cfg

exports.createConnection = (client, server, cfg) => {
    return new Connection(client, server, cfg)
}

logger.add_log_methods(Connection)