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

haraka / Haraka / 26372380144

24 May 2026 08:48PM UTC coverage: 73.594% (+0.02%) from 73.576%
26372380144

push

github

web-flow
Release 3.2.0 (#3566)

- dep: replace address-rfc282(1,2) with @haraka/email-address

1708 of 2205 branches covered (77.46%)

67 of 67 new or added lines in 9 files covered. (100.0%)

14 existing lines in 3 files now uncovered.

8021 of 10899 relevant lines covered (73.59%)

23.4 hits per line

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

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

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

117✔
9
const smtp_client_mod = require('../../smtp_client')
117✔
10
const tls_socket = require('../../tls_socket')
117✔
11

117✔
12
exports.register = function () {
117✔
13
    this.load_errs = []
117✔
14

117✔
15
    this.load_smtp_forward_ini()
117✔
16

117✔
17
    if (this.load_errs.length > 0) return
117✔
18

117✔
19
    if (this.cfg.main.check_sender) {
117✔
20
        this.register_hook('mail', 'check_sender')
117✔
21
    }
1✔
22

1✔
23
    if (this.cfg.main.check_recipient) {
117✔
24
        this.register_hook('rcpt', 'check_recipient')
117✔
25
    }
2✔
26

2✔
27
    this.register_hook('queue', 'queue_forward')
117✔
28

83✔
29
    if (this.cfg.main.enable_outbound) {
83✔
30
        // deliver local message via smtp forward when relaying=true
117✔
31
        this.register_hook('queue_outbound', 'queue_forward')
2✔
32
    }
2✔
33

2✔
34
    // may specify more specific [per-domain] outbound routes
117✔
35
    this.register_hook('get_mx', 'get_mx')
83✔
36
}
83✔
37

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

×
60
    // Build backend TLS options from tls.ini [main] + this plugin's [tls] section.
115✔
61
    // Re-derived on every (re)load so SIGHUP picks up edits.
115✔
62
    this.tls_options = tls_socket.load_plugin_tls_options(this.cfg.tls || {})
115✔
63
}
115✔
64

115✔
65
exports.get_config = function (conn) {
117!
66
    if (!conn.transaction) return this.cfg.main
26✔
67

1✔
68
    let dom, address
26✔
69
    if (this.cfg.main.domain_selector === 'mail_from') {
25✔
70
        if (!conn.transaction.mail_from) return this.cfg.main
26✔
71
        dom = conn.transaction.mail_from.host
4✔
72
        address = conn.transaction.mail_from.address
3✔
73
    } else {
3✔
74
        if (!conn.transaction.rcpt_to[0]) return this.cfg.main
26✔
75
        dom = conn.transaction.rcpt_to[0].host
21✔
76
    }
20✔
77

20✔
78
    if (address && this.cfg[address]) return this.cfg[address]
26✔
79
    if (!dom) return this.cfg.main
26!
80
    if (!this.cfg[dom]) return this.cfg.main // no specific route
26✔
81

7✔
82
    return this.cfg[dom]
7✔
83
}
7✔
84

7✔
85
exports.is_outbound_enabled = function (dom_cfg) {
117✔
86
    if ('enable_outbound' in dom_cfg) return dom_cfg.enable_outbound // per-domain flag
8✔
87

2✔
88
    return this.cfg.main.enable_outbound // follow the global configuration
2✔
89
}
2✔
90

2✔
91
exports.check_sender = function (next, connection, params) {
117✔
92
    const txn = connection?.transaction
5✔
93
    if (!txn) return
5✔
94

5✔
95
    const email = params[0].address
5✔
96
    if (!email) {
4✔
97
        txn.results.add(this, { skip: 'mail_from.null', emit: true })
5!
UNCOV
98
        return next()
×
UNCOV
99
    }
×
UNCOV
100

×
101
    const domain = params[0].host.toLowerCase()
5✔
102
    if (!this.cfg[domain]) return next()
4✔
103

2✔
104
    // domain is defined in smtp_forward.ini
2✔
105
    txn.notes.local_sender = true
2✔
106

2✔
107
    if (!connection.relaying) {
2✔
108
        txn.results.add(this, { fail: 'mail_from!spoof' })
5✔
109
        return next(DENY, 'Spoofed MAIL FROM')
1✔
110
    }
1✔
111

1✔
112
    txn.results.add(this, { pass: 'mail_from' })
1✔
113
    next()
1✔
114
}
1✔
115

1✔
116
exports.set_queue = function (connection, queue_wanted, domain) {
117✔
117
    let dom_cfg = this.cfg[domain]
14✔
118
    if (dom_cfg === undefined) dom_cfg = {}
14✔
119

4✔
120
    if (!queue_wanted) queue_wanted = dom_cfg.queue || this.cfg.main.queue
14!
121
    if (!queue_wanted) return true
14✔
122

14✔
123
    let dst_host = dom_cfg.host || this.cfg.main.host
14!
124
    if (dst_host) dst_host = `smtp://${dst_host}`
14✔
125

13✔
126
    const notes = connection?.transaction?.notes
14✔
127
    if (!notes) return false
14✔
128
    if (!notes.get('queue.wants')) {
14✔
129
        notes.set('queue.wants', queue_wanted)
14✔
130
        if (dst_host) notes.set('queue.next_hop', dst_host)
7✔
131
        return true
7✔
132
    }
7✔
133

7✔
134
    // multiple recipients with same destination
14✔
135
    if (notes.get('queue.wants') === queue_wanted) {
6✔
136
        if (!dst_host) return true
14✔
137

5✔
138
        const next_hop = notes.get('queue.next_hop')
5✔
139
        if (!next_hop) return true
4✔
140
        if (next_hop === dst_host) return true
5✔
141
    }
5✔
142

1✔
143
    // multiple recipients with different forward host, soft deny
14✔
144
    return false
3✔
145
}
3✔
146

3✔
147
exports.check_recipient = function (next, connection, params) {
117✔
148
    const txn = connection?.transaction
6✔
149
    if (!txn) return
6✔
150

6✔
151
    const rcpt = params[0]
6✔
152
    if (!rcpt.host) {
5✔
153
        txn.results.add(this, { skip: 'rcpt!domain' })
6!
UNCOV
154
        return next()
×
UNCOV
155
    }
×
UNCOV
156

×
157
    if (connection.relaying && txn.notes.local_sender) {
6✔
158
        this.set_queue(connection, 'outbound')
6✔
159
        txn.results.add(this, { pass: 'relaying local_sender' })
1✔
160
        return next(OK)
1✔
161
    }
1✔
162

1✔
163
    const domain = rcpt.host.toLowerCase()
6✔
164
    if (this.cfg[domain] !== undefined) {
4✔
165
        if (this.set_queue(connection, 'smtp_forward', domain)) {
6✔
166
            txn.results.add(this, { pass: 'rcpt_to' })
2✔
167
            return next(OK)
1✔
168
        }
1✔
169
        txn.results.add(this, { pass: 'rcpt_to.split' })
1✔
170
        return next(DENYSOFT, 'Split transaction, retry soon')
1✔
171
    }
1✔
172

1✔
173
    // the MAIL FROM domain is not local and neither is the RCPT TO
6✔
174
    // Another RCPT plugin may vouch for this recipient.
2✔
175
    txn.results.add(this, { msg: 'rcpt!local' })
2✔
176
    next()
2✔
177
}
2✔
178

2✔
179
exports.auth = function (cfg, connection, smtp_client) {
117✔
180
    connection.loginfo(this, `Configuring authentication for SMTP server ${cfg.host}:${cfg.port}`)
7✔
181
    smtp_client.on('capabilities', () => {
7✔
182
        connection.loginfo(this, 'capabilities received')
7✔
183

7✔
184
        if ('secured' in smtp_client) {
7✔
185
            connection.loginfo(this, 'secured is pending')
7✔
186
            if (smtp_client.secured === false) {
1✔
187
                connection.loginfo(this, 'Waiting for STARTTLS to complete. AUTH postponed')
1✔
188
                return
1✔
189
            }
1✔
190
        }
1✔
191

1✔
192
        function base64(str) {
7✔
193
            const buffer = Buffer.from(str, 'UTF-8')
5✔
194
            return buffer.toString('base64')
5✔
195
        }
5✔
196

5✔
197
        if (cfg.auth_type === 'plain') {
6✔
198
            connection.loginfo(this, `Authenticating with AUTH PLAIN ${cfg.auth_user}`)
7✔
199
            smtp_client.send_command('AUTH', `PLAIN ${base64(`\0${cfg.auth_user}\0${cfg.auth_pass}`)}`)
3✔
200
            return
3✔
201
        }
3✔
202

3✔
203
        if (cfg.auth_type === 'login') {
3✔
204
            smtp_client.authenticating = true
3✔
205
            smtp_client.authenticated = false
3✔
206

3✔
207
            connection.loginfo(this, `Authenticating with AUTH LOGIN ${cfg.auth_user}`)
3✔
208
            smtp_client.send_command('AUTH', 'LOGIN')
3✔
209
            smtp_client.on('auth', () => {
3✔
210
                // do nothing
3✔
211
            })
×
212
            smtp_client.on('auth_username', () => {
3✔
213
                smtp_client.send_command(base64(cfg.auth_user))
3✔
214
            })
1✔
215
            smtp_client.on('auth_password', () => {
3✔
216
                smtp_client.send_command(base64(cfg.auth_pass))
3✔
217
            })
1✔
218
        }
1✔
219
    })
3✔
220
}
3✔
221

3✔
222
exports.forward_enabled = function (conn, dom_cfg) {
117✔
223
    const q_wants = conn.transaction.notes.get('queue.wants')
17✔
224
    if (q_wants && q_wants !== 'smtp_forward') {
17✔
225
        conn.logdebug(this, `skipping, unwanted (${q_wants})`)
17✔
226
        return false
1✔
227
    }
1✔
228

1✔
229
    if (conn.relaying && !this.is_outbound_enabled(dom_cfg)) {
17✔
230
        conn.logdebug(this, 'skipping, outbound disabled')
17✔
231
        return false
2✔
232
    }
2✔
233

2✔
234
    return true
17✔
235
}
14✔
236

14✔
237
exports.queue_forward = function (next, connection) {
117✔
238
    const plugin = this
13✔
239
    if (connection.remote.closed) return
13✔
240
    const txn = connection?.transaction
13✔
241

12✔
242
    const cfg = plugin.get_config(connection)
13✔
243
    if (!plugin.forward_enabled(connection, cfg)) return next()
13✔
244

1✔
245
    smtp_client_mod.get_client_plugin(plugin, connection, cfg, (err, smtp_client) => {
13✔
246
        smtp_client.next = next
11✔
247

11✔
248
        let rcpt = 0
11✔
249

11✔
250
        if (cfg.auth_user) plugin.auth(cfg, connection, smtp_client)
11✔
251

1✔
252
        connection.loginfo(
11✔
253
            plugin,
11✔
254
            `forwarding to ${cfg.forwarding_host_pool ? 'host_pool' : `${cfg.host}:${cfg.port}`}`,
11✔
255
        )
10✔
256

11✔
257
        function get_rs() {
11✔
258
            return txn?.results ?? connection.results
3!
259
        }
×
260

×
261
        function dead_sender() {
11✔
262
            if (smtp_client.is_dead_sender(plugin, connection)) {
14✔
263
                get_rs().add(plugin, { err: 'dead sender' })
14✔
264
                return true
1✔
265
            }
1✔
266
            return false
14✔
267
        }
13✔
268

13✔
269
        function send_rcpt() {
11✔
270
            if (dead_sender() || !txn) return
6✔
271
            if (rcpt === txn.rcpt_to.length) {
6✔
272
                smtp_client.send_command('DATA')
6!
273
                return
×
274
            }
×
275
            smtp_client.send_command('RCPT', `TO:${txn.rcpt_to[rcpt].format(!smtp_client.smtputf8)}`)
6✔
276
            rcpt++
5✔
277
        }
5✔
278

5✔
279
        smtp_client.on('mail', send_rcpt)
11✔
280

11✔
281
        if (cfg.one_message_per_rcpt) {
11✔
282
            smtp_client.on('rcpt', () => {
11✔
283
                smtp_client.send_command('DATA')
10✔
284
            })
4✔
285
        } else {
10✔
286
            smtp_client.on('rcpt', send_rcpt)
11✔
287
        }
1✔
288

1✔
289
        smtp_client.on('data', () => {
11✔
290
            if (dead_sender()) return
11✔
291
            smtp_client.start_data(txn.message_stream)
3!
292
        })
3✔
293

3✔
294
        smtp_client.on('dot', () => {
11✔
295
            if (dead_sender() || !txn) return
11✔
296

2✔
297
            get_rs().add(plugin, { pass: smtp_client.response })
2!
298
            if (rcpt < txn.rcpt_to.length) {
2✔
299
                smtp_client.send_command('RSET')
2✔
300
                return
1✔
301
            }
1✔
302
            smtp_client.call_next(OK, smtp_client.response)
1✔
303
            smtp_client.release()
1✔
304
        })
1✔
305

1✔
306
        smtp_client.on('rset', () => {
11✔
307
            if (dead_sender() || !txn) return
11✔
308
            smtp_client.send_command('MAIL', `FROM:${txn.mail_from}`)
1!
309
        })
1✔
310

1✔
311
        smtp_client.on('bad_code', (code, msg) => {
11✔
312
            if (dead_sender() || !txn) return
2✔
313
            smtp_client.call_next(code && code[0] === '5' ? DENY : DENYSOFT, msg)
2!
314
            smtp_client.release()
2✔
315
        })
2✔
316
    })
2✔
317
}
2✔
318

2✔
319
exports.get_mx_next_hop = (next_hop) => {
117✔
320
    // queue.wants && queue.next_hop are mechanisms for fine-grained MX routing.
6✔
321
    // Plugins can specify a queue to perform the delivery as well as a route. A
6✔
322
    // plugin that uses this is qmail-deliverable, which can direct email delivery
6✔
323
    // via smtp_forward, outbound (SMTP), and outbound (LMTP).
6✔
324
    const dest = new url.URL(next_hop)
6✔
325
    const mx = {
6✔
326
        priority: 0,
6✔
327
        port: dest.port || (dest.protocol === 'lmtp:' ? 24 : 25),
6✔
328
        exchange: dest.hostname,
6✔
329
    }
6✔
330
    if (dest.protocol === 'lmtp:') mx.using_lmtp = true
6✔
331
    if (dest.username) {
6✔
332
        mx.auth_type = 'plain'
6✔
333
        mx.auth_user = dest.username
1✔
334
        mx.auth_pass = dest.password
1✔
335
    }
1✔
336
    return mx
6✔
337
}
6✔
338

6✔
339
exports.get_mx = function (next, hmail, domain) {
117✔
340
    const qw = hmail.todo.notes.get('queue.wants')
8✔
341
    if (qw && !['smtp_forward', 'outbound'].includes(qw)) return next()
8✔
342

1✔
343
    if (qw === 'smtp_forward' && hmail.todo.notes.get('queue.next_hop')) {
8✔
344
        return next(OK, this.get_mx_next_hop(hmail.todo.notes.get('queue.next_hop')))
8✔
345
    }
2✔
346

2✔
347
    const dom =
8✔
348
        this.cfg.main.domain_selector === 'mail_from' ? hmail.todo.mail_from.host.toLowerCase() : domain.toLowerCase()
8✔
349
    const cfg = this.cfg[dom]
8✔
350

8✔
351
    if (cfg === undefined) {
8✔
352
        this.logdebug(`using DNS MX for: ${domain}`)
8✔
353
        return next()
2✔
354
    }
2✔
355

2✔
356
    const mx_opts = ['auth_type', 'auth_user', 'auth_pass', 'bind', 'bind_helo', 'using_lmtp']
8✔
357

3✔
358
    const mx = {
3✔
359
        priority: 0,
3✔
360
        exchange: cfg.host || this.cfg.main.host,
8!
361
        port: cfg.port || this.cfg.main.port || 25,
8✔
362
    }
3✔
363

×
364
    // apply auth/mx options
8✔
365
    for (const o of mx_opts) {
8✔
366
        if (cfg[o] === undefined) continue
8✔
367
        mx[o] = this.cfg[dom][o]
18✔
368
    }
8✔
369

8✔
370
    next(OK, mx)
8✔
371
}
3✔
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