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

haraka / Haraka / 23884036672

02 Apr 2026 04:35AM UTC coverage: 66.364% (+2.7%) from 63.652%
23884036672

Pull #3546

github

web-flow
Merge de76cdab7 into a9c0fd522
Pull Request #3546: test: add tests for tls_socket & smtp_client

1294 of 1794 branches covered (72.13%)

18 of 24 new or added lines in 3 files covered. (75.0%)

11 existing lines in 2 files now uncovered.

6359 of 9582 relevant lines covered (66.36%)

23.81 hits per line

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

34.44
/plugins/queue/smtp_forward.js
1
'use strict'
50✔
2
// Forward to an SMTP server
50✔
3
// Opens the connection to the ongoing SMTP server at queue time
50✔
4
// and passes back any errors seen on the ongoing server to the
50✔
5
// originating server.
50✔
6

50✔
7
const url = require('node:url')
50✔
8

50✔
9
const smtp_client_mod = require('../../smtp_client')
50✔
10

50✔
11
exports.register = function () {
50✔
12
    this.load_errs = []
51✔
13

51✔
14
    this.load_smtp_forward_ini()
51✔
15

51✔
16
    if (this.load_errs.length > 0) return
51✔
17

51✔
18
    if (this.cfg.main.check_sender) {
51!
19
        this.register_hook('mail', 'check_sender')
51!
20
    }
×
21

×
22
    if (this.cfg.main.check_recipient) {
51✔
23
        this.register_hook('rcpt', 'check_recipient')
51!
24
    }
×
25

×
26
    this.register_hook('queue', 'queue_forward')
51✔
27

51✔
28
    if (this.cfg.main.enable_outbound) {
51✔
29
        // deliver local message via smtp forward when relaying=true
51!
30
        this.register_hook('queue_outbound', 'queue_forward')
×
31
    }
×
32

×
33
    // may specify more specific [per-domain] outbound routes
51✔
34
    this.register_hook('get_mx', 'get_mx')
51✔
35
}
51✔
36

51✔
37
exports.load_smtp_forward_ini = function () {
50✔
38
    this.cfg = this.config.get(
51✔
39
        'smtp_forward.ini',
51✔
40
        {
51✔
41
            booleans: [
51✔
42
                '-main.enable_tls',
51✔
43
                '-main.enable_outbound',
51✔
44
                'main.one_message_per_rcpt',
51✔
45
                '-main.check_sender',
51✔
46
                '-main.check_recipient',
51✔
47
                '*.enable_tls',
51✔
48
                '*.enable_outbound',
51✔
49
            ],
51✔
50
        },
51✔
51
        () => {
51✔
52
            this.load_smtp_forward_ini()
51✔
53
        },
×
54
    )
×
55
}
×
56

×
57
exports.get_config = function (conn) {
50✔
58
    if (!conn.transaction) return this.cfg.main
11!
59

×
60
    let dom, address
11✔
61
    if (this.cfg.main.domain_selector === 'mail_from') {
11✔
62
        if (!conn.transaction.mail_from) return this.cfg.main
11!
63
        dom = conn.transaction.mail_from.host
2✔
64
        address = conn.transaction.mail_from.address()
2✔
65
    } else {
2✔
66
        if (!conn.transaction.rcpt_to[0]) return this.cfg.main
11✔
67
        dom = conn.transaction.rcpt_to[0].host
9✔
68
    }
8✔
69

8✔
70
    if (address && this.cfg[address]) return this.cfg[address]
11!
71
    if (!dom) return this.cfg.main
11✔
72
    if (!this.cfg[dom]) return this.cfg.main // no specific route
11✔
73

7✔
74
    return this.cfg[dom]
7✔
75
}
7✔
76

7✔
77
exports.is_outbound_enabled = function (dom_cfg) {
50✔
78
    if ('enable_outbound' in dom_cfg) return dom_cfg.enable_outbound // per-domain flag
4✔
79

2✔
80
    return this.cfg.main.enable_outbound // follow the global configuration
2✔
81
}
2✔
82

2✔
83
exports.check_sender = function (next, connection, params) {
50✔
84
    const txn = connection?.transaction
×
85
    if (!txn) return
×
86

×
87
    const email = params[0].address()
×
88
    if (!email) {
×
89
        txn.results.add(this, { skip: 'mail_from.null', emit: true })
×
90
        return next()
×
91
    }
×
92

×
93
    const domain = params[0].host.toLowerCase()
×
94
    if (!this.cfg[domain]) return next()
×
95

×
96
    // domain is defined in smtp_forward.ini
×
97
    txn.notes.local_sender = true
×
98

×
99
    if (!connection.relaying) {
×
100
        txn.results.add(this, { fail: 'mail_from!spoof' })
×
101
        return next(DENY, 'Spoofed MAIL FROM')
×
102
    }
×
103

×
104
    txn.results.add(this, { pass: 'mail_from' })
×
105
    next()
×
106
}
×
107

×
108
exports.set_queue = function (connection, queue_wanted, domain) {
50✔
109
    let dom_cfg = this.cfg[domain]
×
110
    if (dom_cfg === undefined) dom_cfg = {}
×
111

×
112
    if (!queue_wanted) queue_wanted = dom_cfg.queue || this.cfg.main.queue
×
113
    if (!queue_wanted) return true
×
114

×
115
    let dst_host = dom_cfg.host || this.cfg.main.host
×
116
    if (dst_host) dst_host = `smtp://${dst_host}`
×
117

×
118
    const notes = connection?.transaction?.notes
×
119
    if (!notes) return false
×
120
    if (!notes.get('queue.wants')) {
×
121
        notes.set('queue.wants', queue_wanted)
×
122
        if (dst_host) notes.set('queue.next_hop', dst_host)
×
123
        return true
×
124
    }
×
125

×
126
    // multiple recipients with same destination
×
127
    if (notes.get('queue.wants') === queue_wanted) {
×
128
        if (!dst_host) return true
×
129

×
130
        const next_hop = notes.get('queue.next_hop')
×
131
        if (!next_hop) return true
×
132
        if (next_hop === dst_host) return true
×
133
    }
×
134

×
135
    // multiple recipients with different forward host, soft deny
×
136
    return false
×
137
}
×
138

×
139
exports.check_recipient = function (next, connection, params) {
50✔
140
    const txn = connection?.transaction
×
141
    if (!txn) return
×
142

×
143
    const rcpt = params[0]
×
144
    if (!rcpt.host) {
×
145
        txn.results.add(this, { skip: 'rcpt!domain' })
×
146
        return next()
×
147
    }
×
148

×
149
    if (connection.relaying && txn.notes.local_sender) {
×
150
        this.set_queue(connection, 'outbound')
×
151
        txn.results.add(this, { pass: 'relaying local_sender' })
×
152
        return next(OK)
×
153
    }
×
154

×
155
    const domain = rcpt.host.toLowerCase()
×
156
    if (this.cfg[domain] !== undefined) {
×
157
        if (this.set_queue(connection, 'smtp_forward', domain)) {
×
158
            txn.results.add(this, { pass: 'rcpt_to' })
×
159
            return next(OK)
×
160
        }
×
161
        txn.results.add(this, { pass: 'rcpt_to.split' })
×
162
        return next(DENYSOFT, 'Split transaction, retry soon')
×
163
    }
×
164

×
165
    // the MAIL FROM domain is not local and neither is the RCPT TO
×
166
    // Another RCPT plugin may vouch for this recipient.
×
167
    txn.results.add(this, { msg: 'rcpt!local' })
×
168
    next()
×
169
}
×
170

×
171
exports.auth = function (cfg, connection, smtp_client) {
50✔
172
    connection.loginfo(this, `Configuring authentication for SMTP server ${cfg.host}:${cfg.port}`)
×
173
    smtp_client.on('capabilities', () => {
×
174
        connection.loginfo(this, 'capabilities received')
×
175

×
176
        if ('secured' in smtp_client) {
×
177
            connection.loginfo(this, 'secured is pending')
×
178
            if (smtp_client.secured === false) {
×
179
                connection.loginfo(this, 'Waiting for STARTTLS to complete. AUTH postponed')
×
180
                return
×
181
            }
×
182
        }
×
183

×
184
        function base64(str) {
×
185
            const buffer = Buffer.from(str, 'UTF-8')
×
186
            return buffer.toString('base64')
×
187
        }
×
188

×
189
        if (cfg.auth_type === 'plain') {
×
190
            connection.loginfo(this, `Authenticating with AUTH PLAIN ${cfg.auth_user}`)
×
191
            smtp_client.send_command('AUTH', `PLAIN ${base64(`\0${cfg.auth_user}\0${cfg.auth_pass}`)}`)
×
192
            return
×
193
        }
×
194

×
195
        if (cfg.auth_type === 'login') {
×
196
            smtp_client.authenticating = true
×
197
            smtp_client.authenticated = false
×
198

×
199
            connection.loginfo(this, `Authenticating with AUTH LOGIN ${cfg.auth_user}`)
×
200
            smtp_client.send_command('AUTH', 'LOGIN')
×
201
            smtp_client.on('auth', () => {
×
202
                // do nothing
×
203
            })
×
204
            smtp_client.on('auth_username', () => {
×
205
                smtp_client.send_command(base64(cfg.auth_user))
×
206
            })
×
207
            smtp_client.on('auth_password', () => {
×
208
                smtp_client.send_command(base64(cfg.auth_pass))
×
209
            })
×
210
        }
×
211
    })
×
212
}
×
213

×
214
exports.forward_enabled = function (conn, dom_cfg) {
50✔
215
    const q_wants = conn.transaction.notes.get('queue.wants')
×
216
    if (q_wants && q_wants !== 'smtp_forward') {
×
217
        conn.logdebug(this, `skipping, unwanted (${q_wants})`)
×
218
        return false
×
219
    }
×
220

×
221
    if (conn.relaying && !this.is_outbound_enabled(dom_cfg)) {
×
222
        conn.logdebug(this, 'skipping, outbound disabled')
×
223
        return false
×
224
    }
×
225

×
226
    return true
×
227
}
×
228

×
229
exports.queue_forward = function (next, connection) {
50✔
230
    const plugin = this
×
231
    if (connection.remote.closed) return
×
232
    const txn = connection?.transaction
×
233

×
234
    const cfg = plugin.get_config(connection)
×
235
    if (!plugin.forward_enabled(connection, cfg)) return next()
×
236

×
237
    smtp_client_mod.get_client_plugin(plugin, connection, cfg, (err, smtp_client) => {
×
238
        smtp_client.next = next
×
239

×
240
        let rcpt = 0
×
241

×
242
        if (cfg.auth_user) plugin.auth(cfg, connection, smtp_client)
×
243

×
244
        connection.loginfo(
×
245
            plugin,
×
246
            `forwarding to ${cfg.forwarding_host_pool ? 'host_pool' : `${cfg.host}:${cfg.port}`}`,
×
247
        )
×
248

×
249
        function get_rs() {
×
NEW
250
            return txn?.results ?? connection.results
×
251
        }
×
252

×
253
        function dead_sender() {
×
254
            if (smtp_client.is_dead_sender(plugin, connection)) {
×
255
                get_rs().add(plugin, { err: 'dead sender' })
×
256
                return true
×
257
            }
×
258
            return false
×
259
        }
×
260

×
261
        function send_rcpt() {
×
262
            if (dead_sender() || !txn) return
×
263
            if (rcpt === txn.rcpt_to.length) {
×
264
                smtp_client.send_command('DATA')
×
265
                return
×
266
            }
×
267
            smtp_client.send_command('RCPT', `TO:${txn.rcpt_to[rcpt].format(!smtp_client.smtp_utf8)}`)
×
268
            rcpt++
×
269
        }
×
270

×
271
        smtp_client.on('mail', send_rcpt)
×
272

×
273
        if (cfg.one_message_per_rcpt) {
×
274
            smtp_client.on('rcpt', () => {
×
275
                smtp_client.send_command('DATA')
×
276
            })
×
277
        } else {
×
278
            smtp_client.on('rcpt', send_rcpt)
×
279
        }
×
280

×
281
        smtp_client.on('data', () => {
×
282
            if (dead_sender()) return
×
283
            smtp_client.start_data(txn.message_stream)
×
284
        })
×
285

×
286
        smtp_client.on('dot', () => {
×
287
            if (dead_sender() || !txn) return
×
288

×
289
            get_rs().add(plugin, { pass: smtp_client.response })
×
290
            if (rcpt < txn.rcpt_to.length) {
×
291
                smtp_client.send_command('RSET')
×
292
                return
×
293
            }
×
294
            smtp_client.call_next(OK, smtp_client.response)
×
295
            smtp_client.release()
×
296
        })
×
297

×
298
        smtp_client.on('rset', () => {
×
299
            if (dead_sender() || !txn) return
×
300
            smtp_client.send_command('MAIL', `FROM:${txn.mail_from}`)
×
301
        })
×
302

×
303
        smtp_client.on('bad_code', (code, msg) => {
×
304
            if (dead_sender() || !txn) return
×
305
            smtp_client.call_next(code && code[0] === '5' ? DENY : DENYSOFT, msg)
×
306
            smtp_client.release()
×
307
        })
×
308
    })
×
309
}
×
310

×
311
exports.get_mx_next_hop = (next_hop) => {
50✔
312
    // queue.wants && queue.next_hop are mechanisms for fine-grained MX routing.
2✔
313
    // Plugins can specify a queue to perform the delivery as well as a route. A
2✔
314
    // plugin that uses this is qmail-deliverable, which can direct email delivery
2✔
315
    // via smtp_forward, outbound (SMTP), and outbound (LMTP).
2✔
316
    const dest = new url.URL(next_hop)
2✔
317
    const mx = {
2✔
318
        priority: 0,
2✔
319
        port: dest.port || (dest.protocol === 'lmtp:' ? 24 : 25),
2✔
320
        exchange: dest.hostname,
2!
321
    }
2✔
322
    if (dest.protocol === 'lmtp:') mx.using_lmtp = true
2✔
323
    if (dest.auth) {
2✔
324
        mx.auth_type = 'plain'
2!
325
        mx.auth_user = dest.auth.split(':')[0]
×
326
        mx.auth_pass = dest.auth.split(':')[1]
×
UNCOV
327
    }
×
328
    return mx
2✔
329
}
2✔
330

2✔
331
exports.get_mx = function (next, hmail, domain) {
50✔
332
    const qw = hmail.todo.notes.get('queue.wants')
5✔
333
    if (qw && !['smtp_forward', 'outbound'].includes(qw)) return next()
5✔
334

×
335
    if (qw === 'smtp_forward' && hmail.todo.notes.get('queue.next_hop')) {
5✔
336
        return next(OK, this.get_mx_next_hop(hmail.todo.notes.get('queue.next_hop')))
5✔
337
    }
2✔
338

2✔
339
    const dom =
5✔
340
        this.cfg.main.domain_selector === 'mail_from' ? hmail.todo.mail_from.host.toLowerCase() : domain.toLowerCase()
5!
341
    const cfg = this.cfg[dom]
5✔
342

5✔
343
    if (cfg === undefined) {
5✔
344
        this.logdebug(`using DNS MX for: ${domain}`)
5✔
345
        return next()
2✔
346
    }
2✔
347

2✔
348
    const mx_opts = ['auth_type', 'auth_user', 'auth_pass', 'bind', 'bind_helo', 'using_lmtp']
5✔
349

1✔
350
    const mx = {
1✔
351
        priority: 0,
1✔
352
        exchange: cfg.host || this.cfg.main.host,
5!
353
        port: cfg.port || this.cfg.main.port || 25,
5✔
354
    }
1✔
355

5✔
356
    // apply auth/mx options
5!
357
    mx_opts.forEach((o) => {
5✔
358
        if (cfg[o] === undefined) return
5✔
359
        mx[o] = this.cfg[dom][o]
6✔
360
    })
2✔
361

2✔
362
    next(OK, mx)
5✔
363
}
5✔
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