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

haraka / Haraka / 29801972288

21 Jul 2026 04:42AM UTC coverage: 72.501% (+0.2%) from 72.32%
29801972288

Pull #3608

github

web-flow
Merge 00b7f8e39 into e9ee7bce1
Pull Request #3608: Release v3.3.2

1682 of 2214 branches covered (75.97%)

7 of 9 new or added lines in 3 files covered. (77.78%)

38 existing lines in 4 files now uncovered.

7601 of 10484 relevant lines covered (72.5%)

25.95 hits per line

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

67.96
/server.js
1
'use strict'
2✔
2
// smtp network server
2✔
3

2✔
4
const cluster = require('node:cluster')
2✔
5
const { spawn } = require('node:child_process')
2✔
6
const fs = require('node:fs')
2✔
7
const net = require('node:net')
2✔
8
const os = require('node:os')
2✔
9
const path = require('node:path')
2✔
10
const tls = require('node:tls')
2✔
11
const constants = require('haraka-constants')
2✔
12
const net_utils = require('haraka-net-utils')
2✔
13

2✔
14
const { endpoint } = require('haraka-net-utils')
2✔
15
const tls_socket = require('./tls_socket')
2✔
16
const conn = require('./connection')
2✔
17
const outbound = require('./outbound')
2✔
18

2✔
19
const Server = exports
2✔
20
Server.logger = require('./logger')
2✔
21
Server.config = require('haraka-config')
2✔
22
Server.plugins = require('./plugins')
2✔
23
Server.notes = {}
2✔
24

2✔
25
const { logger } = Server
2✔
26

2✔
27
// Need these here so we can run hooks
2✔
28
logger.add_log_methods(Server, 'server')
2✔
29

2✔
30
Server.listeners = []
2✔
31

2✔
32
Server.load_smtp_ini = () => {
2✔
33
    Server.cfg = Server.config.get(
9✔
34
        'smtp.ini',
9✔
35
        {
9✔
36
            booleans: ['-main.daemonize', '-main.graceful_shutdown'],
9✔
37
        },
9✔
38
        () => {
9✔
39
            Server.load_smtp_ini()
×
40
        },
9✔
41
    )
9✔
42

9✔
43
    if (Server.cfg.main.nodes === undefined) {
9✔
44
        Server.logwarn(`smtp.ini.nodes unset, using 1, see https://github.com/haraka/Haraka/wiki/Performance-Tuning`)
1✔
45
    }
1✔
46

9✔
47
    const defaults = {
9✔
48
        inactivity_timeout: 300,
9✔
49
        daemon_log_file: '/var/log/haraka.log',
9✔
50
        daemon_pid_file: '/var/run/haraka.pid',
9✔
51
        force_shutdown_timeout: 30,
9✔
52
        smtps_port: 465,
9✔
53
        nodes: 1,
9✔
54
    }
9✔
55

9✔
56
    for (const key in defaults) {
9✔
57
        if (Server.cfg.main[key] !== undefined) continue
54✔
58
        Server.cfg.main[key] = defaults[key]
11✔
59
    }
11✔
60
}
9✔
61

2✔
62
Server.load_http_ini = () => {
2✔
63
    Server.http = {}
2✔
64
    Server.http.cfg = Server.config.get('http.ini', () => {
2✔
65
        Server.load_http_ini()
×
66
    }).main
2✔
67
}
2✔
68

2✔
69
Server.load_connection_ini = () => {
2✔
70
    Server.connection = {}
2✔
71
    Server.connection.cfg = Server.config.get('connection.ini', {
2✔
72
        booleans: ['+haproxy.enabled'],
2✔
73
    })
2✔
74
}
2✔
75

2✔
76
Server.load_smtp_ini()
2✔
77
Server.load_http_ini()
2✔
78
Server.load_connection_ini()
2✔
79

2✔
80
Server.daemonize = function () {
2✔
81
    const c = this.cfg.main
6✔
82
    if (!c.daemonize) return
6!
83

×
84
    if (!process.env.__daemon) {
×
85
        // Remove process.on('exit') listeners otherwise
×
86
        // we get a spurious 'Exiting' log entry.
×
87
        process.removeAllListeners('exit')
×
88
        Server.lognotice('Daemonizing...')
×
89

×
90
        const log_fd = fs.openSync(c.daemon_log_file, 'a')
×
91
        const child = spawn(process.execPath, process.argv.slice(1), {
×
92
            detached: true,
×
93
            stdio: ['ignore', log_fd, log_fd],
×
94
            env: { ...process.env, __daemon: '1' },
×
95
            cwd: process.cwd(),
×
96
        })
×
97
        child.unref()
×
98
        process.exit(0)
×
99
    }
×
100

×
101
    // We are the daemon from here on...
×
102
    try {
×
103
        fs.writeFileSync(c.daemon_pid_file, `${process.pid}\n`, { flag: 'wx' })
×
104
        process.on('exit', () => {
×
105
            try {
×
106
                fs.unlinkSync(c.daemon_pid_file)
×
107
            } catch {}
×
108
        })
×
109
    } catch (err) {
×
110
        Server.logerror(err.message)
×
111
        logger.dump_and_exit(1)
×
112
    }
×
113
}
6✔
114

2✔
115
Server.flushQueue = async (domain) => {
2✔
116
    if (!Server.cluster) {
×
117
        await outbound.flush_queue(domain)
×
118
        return
×
119
    }
×
120

×
121
    for (const id in cluster.workers) {
×
122
        cluster.workers[id].send({ event: 'outbound.flush_queue', domain })
×
123
    }
×
124
}
×
125

2✔
126
let graceful_in_progress = false
2✔
127

2✔
128
Server.gracefulRestart = () => {
2✔
129
    Server._graceful()
×
130
}
×
131

2✔
132
Server.stopListeners = () => {
2✔
133
    Server.loginfo('Shutting down listeners')
6✔
134
    for (const l of Server.listeners) {
6✔
135
        l.close()
17✔
136
    }
17✔
137
    Server.listeners = []
6✔
138
}
6✔
139

2✔
140
Server.performShutdown = () => {
2✔
141
    if (Server.cfg.main.graceful_shutdown) {
×
142
        return Server.gracefulShutdown()
×
143
    }
×
144
    Server.loginfo('Shutting down.')
×
145
    process.exit(0)
×
146
}
×
147

2✔
148
Server.gracefulShutdown = () => {
2✔
149
    Server.stopListeners()
×
150
    Server._graceful(() => {
×
151
        // log();
×
152
        Server.loginfo('Failed to shutdown naturally. Exiting.')
×
153
        process.exit(0)
×
154
    })
×
155
}
×
156

2✔
157
Server._graceful = async (shutdown) => {
2✔
158
    if (!Server.cluster && shutdown) {
1!
159
        for (const module of ['outbound', 'cfreader', 'plugins']) {
×
160
            process.emit('message', { event: `${module}.shutdown` })
×
161
        }
×
162
        const t = setTimeout(shutdown, Server.cfg.main.force_shutdown_timeout * 1000)
×
163
        return t.unref()
×
164
    }
×
165

1✔
166
    if (graceful_in_progress) {
1!
167
        Server.lognotice('Restart currently in progress - ignoring request')
×
168
        return
×
169
    }
×
170

1✔
171
    graceful_in_progress = true
1✔
172
    // TODO: Make these configurable
1✔
173
    const disconnect_timeout = 30
1✔
174
    const exit_timeout = 30
1✔
175
    cluster.removeAllListeners('exit')
1✔
176

1✔
177
    // we reload using eachLimit where limit = num_workers - 1
1✔
178
    // this kills all-but-one workers in parallel, leaving one running
1✔
179
    // for new connections, and then restarts that one last worker.
1✔
180

1✔
181
    const worker_ids = Object.keys(cluster.workers)
1✔
182
    let limit = worker_ids.length - 1
1✔
183
    if (limit < 2) limit = 1
1✔
184

1✔
185
    const todo = []
1✔
186

1✔
187
    for (const id of Object.keys(cluster.workers)) {
1✔
188
        todo.push(() => {
1✔
189
            return new Promise((resolve) => {
1✔
190
                Server.lognotice(`Killing worker: ${id}`)
1✔
191
                const worker = cluster.workers[id]
1✔
192
                for (const module of ['outbound', 'cfreader', 'plugins']) {
1✔
193
                    worker.send({ event: `${module}.shutdown` })
3✔
194
                }
3✔
195
                worker.disconnect()
1✔
196
                let disconnect_received = false
1✔
197
                const disconnect_timer = setTimeout(() => {
1✔
198
                    if (!disconnect_received) {
×
199
                        Server.logcrit('Disconnect never received by worker. Killing.')
×
200
                        worker.kill()
×
201
                    }
×
202
                }, disconnect_timeout * 1000)
1✔
203

1✔
204
                worker.once('disconnect', () => {
1✔
205
                    clearTimeout(disconnect_timer)
1✔
206
                    disconnect_received = true
1✔
207
                    Server.lognotice('Disconnect complete')
1✔
208
                    let dead = false
1✔
209
                    const timer = setTimeout(() => {
1✔
210
                        if (!dead) {
×
211
                            Server.logcrit(`Worker ${id} failed to shutdown. Killing.`)
×
212
                            worker.kill()
×
213
                        }
×
214
                    }, exit_timeout * 1000)
1✔
215
                    worker.once('exit', () => {
1✔
216
                        dead = true
1✔
217
                        clearTimeout(timer)
1✔
218
                        if (shutdown) resolve()
1!
219
                    })
1✔
220
                })
1✔
221
                if (!shutdown) {
1✔
222
                    const newWorker = cluster.fork()
1✔
223
                    newWorker.once('listening', () => {
1✔
224
                        Server.lognotice('Replacement worker online.')
1✔
225
                        newWorker.on('exit', (code, signal) => {
1✔
226
                            cluster_exit_listener(newWorker, code, signal)
×
227
                        })
1✔
228
                        resolve()
1✔
229
                    })
1✔
230
                }
1✔
231
            })
1✔
232
        })
1✔
233
    }
1✔
234

1✔
235
    while (todo.length) {
1✔
236
        // process batches of workers: invoke each queued thunk so we
1✔
237
        // actually await the worker shutdown promises (passing the bare
1✔
238
        // functions to Promise.all would resolve immediately).
1✔
239
        await Promise.all(todo.splice(0, limit).map((fn) => fn()))
1✔
240
    }
1✔
241

1✔
242
    if (shutdown) {
1!
243
        Server.loginfo('Workers closed. Shutting down master process subsystems')
×
244
        for (const module of ['outbound', 'cfreader', 'plugins']) {
×
245
            process.emit('message', { event: `${module}.shutdown` })
×
246
        }
×
247
        const t2 = setTimeout(shutdown, Server.cfg.main.force_shutdown_timeout * 1000)
×
248
        return t2.unref()
×
249
    }
×
250
    graceful_in_progress = false
1✔
251
    Server.lognotice(`Reload complete, workers: ${JSON.stringify(Object.keys(cluster.workers))}`)
1✔
252
}
1✔
253

2✔
254
Server.sendToMaster = (command, params) => {
2✔
255
    // console.log("Send to master: ", command);
1✔
256
    if (Server.cluster) {
1!
257
        if (Server.cluster.isMaster) {
×
258
            Server.receiveAsMaster(command, params)
×
259
        } else {
×
260
            process.send({ cmd: command, params })
×
261
        }
×
262
    } else {
1✔
263
        Server.receiveAsMaster(command, params)
1✔
264
    }
1✔
265
}
1✔
266

2✔
267
Server.receiveAsMaster = (command, params) => {
2✔
268
    if (!Server[command]) {
2✔
269
        Server.logerror(`Invalid command: ${command}`)
1✔
270
        return
1✔
271
    }
1✔
272
    Server[command].apply(Server, params)
1✔
273
}
2✔
274

2✔
275
function messageHandler(worker, msg) {
×
276
    if (msg?.cmd) {
×
277
        Server.receiveAsMaster(msg.cmd, msg.params)
×
278
    }
×
279
}
×
280

2✔
281
Server.get_listen_addrs = (cfg, port) => {
2✔
282
    if (!port) port = 25
15✔
283
    let listeners = []
15✔
284
    if (cfg?.listen) {
15✔
285
        listeners = cfg.listen.split(/\s*,\s*/)
15✔
286
        if (listeners[0] === '') listeners = []
15!
287
        for (let i = 0; i < listeners.length; i++) {
15✔
288
            const ep = endpoint(listeners[i], port)
18✔
289
            if (ep instanceof Error) continue
18!
290
            listeners[i] = ep.toString()
18✔
291
        }
18✔
292
    }
15✔
293
    if (cfg.port) {
15!
294
        let host = cfg.listen_host
×
295
        if (!host) {
×
296
            host = '[::0]'
×
297
            Server.default_host = true
×
298
        }
×
299
        listeners.unshift(`${host}:${cfg.port}`)
×
300
    }
×
301
    if (listeners.length) return listeners
15!
302

×
303
    Server.default_host = true
×
304
    listeners.push(`[::0]:${port}`)
×
305

×
306
    return listeners
×
307
}
15✔
308

2✔
309
Server.createServer = (params) => {
2✔
310
    const c = Server.cfg.main
6✔
311
    for (const key in params) {
6!
312
        if (typeof params[key] === 'function') continue
×
313
        c[key] = params[key]
×
314
    }
×
315

6✔
316
    Server.notes = {}
6✔
317
    Server.plugins.server = Server
6✔
318
    Server.plugins.load_plugins()
6✔
319

6✔
320
    const inactivity_timeout = (c.inactivity_timeout || 300) * 1000
6!
321

6✔
322
    if (!cluster || !c.nodes) {
6✔
323
        Server.daemonize(c)
6✔
324
        Server.setup_smtp_listeners(Server.plugins, 'master', inactivity_timeout)
6✔
325
        return
6✔
326
    }
6!
327

×
328
    // Cluster
×
329
    Server.cluster = cluster
×
330

×
331
    // Cluster Workers
×
332
    if (!cluster.isMaster) {
×
333
        Server.setup_smtp_listeners(Server.plugins, 'child', inactivity_timeout)
×
334
        return
×
335
    } else {
×
336
        // console.log("Setting up message handler");
×
337
        cluster.on('message', messageHandler)
×
338
    }
×
339

×
340
    // Cluster Master
×
341
    // We fork workers in init_master_respond so that plugins
×
342
    // can put handlers on cluster events before they are emitted.
×
343
    Server.plugins.run_hooks('init_master', Server)
×
344
}
6✔
345

2✔
346
Server.load_default_tls_config = (done) => {
2✔
347
    // this fn exists solely for testing
17✔
348
    if (Server.config.root_path != tls_socket.config.root_path) {
17✔
349
        Server.loginfo(`resetting tls_config.config path to ${Server.config.root_path}`)
1✔
350
        tls_socket.config = tls_socket.config.module_config(path.dirname(Server.config.root_path))
1✔
351
    }
1✔
352
    tls_socket.getSocketOpts('*').then((opts) => {
17✔
353
        done(opts)
17✔
354
    })
17✔
355
}
17✔
356

2✔
357
Server.create_smtps_server = (opts, onConnect) => {
2✔
358
    let server
10✔
359
    const socket_tls_state = new Map()
10✔
360
    const proxyPrefix = Buffer.from('PROXY ')
10✔
361
    // Defensive cap while waiting for a PROXY v1 line before TLS starts.
10✔
362
    const proxyLineReadLimit = 512
10✔
363

10✔
364
    const tlsServer = tls.createServer(opts, (cleartext) => {
10✔
365
        const state_key = socket_tls_state_key(cleartext)
4✔
366
        const smtps_state = socket_tls_state.get(state_key)
4✔
367
        if (smtps_state) cleartext.haraka_smtps = smtps_state
4✔
368
        socket_tls_state.delete(state_key)
4✔
369

4✔
370
        onConnect(cleartext)
4✔
371
    })
10✔
372

10✔
373
    function close_with_proxy_error(socket, timer, msg) {
10✔
374
        clearTimeout(timer)
3✔
375
        socket.removeAllListeners('data')
3✔
376
        socket.end(`421 ${msg}\r\n`, () => {
3✔
377
            socket.destroy()
3✔
378
        })
3✔
379
    }
3✔
380

10✔
381
    function socket_tls_state_key(socket) {
10✔
382
        return JSON.stringify([socket.remoteAddress, socket.remotePort, socket.localAddress, socket.localPort])
10✔
383
    }
10✔
384

10✔
385
    function start_tls(socket, proxy, peer_allowed) {
10✔
386
        if (proxy || peer_allowed) {
7✔
387
            const smtps_state = { peer_allowed }
3✔
388
            if (proxy) {
3✔
389
                smtps_state.proxy = {
1✔
390
                    ...proxy,
1✔
391
                    proxy_ip: net_utils.normalize_ip(socket.remoteAddress) || socket.remoteAddress,
1!
392
                }
1✔
393
            }
1✔
394
            socket_tls_state.set(socket_tls_state_key(socket), smtps_state)
3✔
395
        }
3✔
396

7✔
397
        tlsServer.emit('connection', socket)
7✔
398
    }
7✔
399

10✔
400
    tlsServer.on('tlsClientError', (err, cleartext) => {
10✔
401
        if (cleartext) socket_tls_state.delete(socket_tls_state_key(cleartext))
3✔
402
        server.emit('tlsClientError', err, cleartext)
3✔
403
    })
10✔
404

10✔
405
    tlsServer.on('secureConnection', (cleartext) => {
10✔
406
        server.emit('secureConnection', cleartext)
4✔
407
    })
10✔
408

10✔
409
    function starts_with_proxy_prefix(data) {
10✔
410
        if (!data.length) return true
5!
411
        if (data.length > proxyPrefix.length) return data.subarray(0, proxyPrefix.length).equals(proxyPrefix)
5!
412

×
413
        return proxyPrefix.subarray(0, data.length).equals(data)
×
414
    }
5✔
415

10✔
416
    function start_tls_with_buffer(socket, data, proxy, peer_allowed) {
10✔
417
        // Preserve bytes already read by the PROXY pre-parser, then hand the
3✔
418
        // paused socket to TLS before letting it read again.
3✔
419
        socket.pause()
3✔
420
        if (data?.length) socket.unshift(data)
3✔
421
        setImmediate(() => {
3✔
422
            start_tls(socket, proxy, peer_allowed)
3✔
423
            socket.resume()
3✔
424
        })
3✔
425
    }
3✔
426

10✔
427
    server = net.createServer((socket) => {
10✔
428
        const remote_ip = net_utils.normalize_ip(socket.remoteAddress) || socket.remoteAddress
10!
429

10✔
430
        if (!net_utils.is_haproxy_allowed(remote_ip)) {
10✔
431
            start_tls(socket)
4✔
432
            return
4✔
433
        }
4✔
434

6✔
435
        let current_data = null
6✔
436
        const proxy_timer = setTimeout(() => {
6✔
437
            close_with_proxy_error(socket, proxy_timer, 'PROXY timeout')
1✔
438
        }, 30 * 1000)
6✔
439

6✔
440
        function cleanup() {
6✔
441
            clearTimeout(proxy_timer)
6✔
442
            // Stop flowing before removing the pre-parser listener so TLS bytes
6✔
443
            // cannot arrive between listener removal and TLS attachment.
6✔
444
            socket.pause()
6✔
445
            socket.removeListener('data', on_data)
6✔
446
            socket.removeListener('close', cleanup)
6✔
447
            socket.removeListener('error', cleanup)
6✔
448
        }
6✔
449

6✔
450
        function on_data(data) {
6✔
451
            current_data = current_data ? Buffer.concat([current_data, data]) : data
5!
452

5✔
453
            if (!starts_with_proxy_prefix(current_data)) {
5✔
454
                cleanup()
2✔
455
                start_tls_with_buffer(socket, current_data, null, true)
2✔
456
                return
2✔
457
            }
2✔
458

3✔
459
            const offset = current_data.indexOf(0x0a)
3✔
460
            if (offset === -1) {
5✔
461
                if (current_data.length > proxyLineReadLimit) {
1✔
462
                    close_with_proxy_error(socket, proxy_timer, 'Invalid PROXY format')
1✔
463
                }
1✔
464
                return
1✔
465
            }
1✔
466
            if (offset > proxyLineReadLimit) {
5!
467
                close_with_proxy_error(socket, proxy_timer, 'Invalid PROXY format')
×
468
                return
×
469
            }
✔
470

2✔
471
            cleanup()
2✔
472

2✔
473
            const proxy = net_utils.parse_proxy_line(current_data.slice(0, offset + 1))
2✔
474
            if (!proxy) {
5✔
475
                close_with_proxy_error(socket, proxy_timer, 'Invalid PROXY format')
1✔
476
                return
1✔
477
            }
1✔
478

1✔
479
            const rest = current_data.slice(offset + 1)
1✔
480
            start_tls_with_buffer(socket, rest, proxy, true)
1✔
481
        }
5✔
482

6✔
483
        socket.once('close', cleanup)
6✔
484
        socket.once('error', cleanup)
6✔
485
        socket.on('data', on_data)
6✔
486
    })
10✔
487

10✔
488
    server.tlsServer = tlsServer
10✔
489

10✔
490
    return server
10✔
491
}
10✔
492

2✔
493
Server.get_smtp_server = async (ep, inactivity_timeout) => {
2✔
494
    let server
17✔
495

17✔
496
    function onConnect(client) {
17✔
497
        client.setTimeout(inactivity_timeout)
9✔
498
        const connection = conn.createConnection(client, server, Server.cfg)
9✔
499

9✔
500
        if (server.has_tls) {
9✔
501
            const cipher = client.getCipher()
4✔
502
            cipher.version = client.getProtocol() // replace min with actual
4✔
503

4✔
504
            connection.setTLS({
4✔
505
                cipher,
4✔
506
                verified: client.authorized,
4✔
507
                verifyError: client.authorizationError,
4✔
508
                peerCertificate: client.getPeerCertificate(),
4✔
509
            })
4✔
510
        }
4✔
511

9✔
512
        if (client.haraka_smtps?.proxy) connection.apply_proxy(client.haraka_smtps.proxy)
9✔
513
    }
9✔
514

17✔
515
    if (ep.port === parseInt(Server.cfg.main.smtps_port, 10)) {
17✔
516
        Server.loginfo('getting SocketOpts for SMTPS server')
11✔
517
        const opts = await tls_socket.getSocketOpts('*')
11✔
518
        Server.loginfo(`Creating TLS server on ${ep}`)
11✔
519

11✔
520
        opts.rejectUnauthorized = tls_socket.get_rejectUnauthorized(
11✔
521
            opts.rejectUnauthorized,
11✔
522
            ep.port,
11✔
523
            tls_socket.cfg.main.requireAuthorized,
11✔
524
        )
11✔
525

11✔
526
        server = Server.connection.cfg.haproxy.enabled
11✔
527
            ? Server.create_smtps_server(opts, onConnect)
11✔
528
            : tls.createServer(opts, onConnect)
11✔
529
        const tls_event_server = server.tlsServer || server
11✔
530
        tls_socket.addOCSP(tls_event_server)
11✔
531
        server.has_tls = true
11✔
532
        tls_event_server.on('resumeSession', (id, rsDone) => {
11✔
533
            Server.loginfo('client requested TLS resumeSession')
6✔
534
            rsDone(null, null)
6✔
535
        })
11✔
536
        Server.listeners.push(server)
11✔
537
        return server
11✔
538
    } else {
17✔
539
        server = tls_socket.createServer(onConnect)
6✔
540
        server.has_tls = false
6✔
541
        await tls_socket.getSocketOpts('*')
6✔
542
        Server.listeners.push(server)
6✔
543
        return server
6✔
544
    }
6✔
545
}
17✔
546

2✔
547
Server.setup_smtp_listeners = async (plugins2, type, inactivity_timeout) => {
2✔
548
    const errors = []
6✔
549

6✔
550
    for (const [, ifObj] of Object.entries(os.networkInterfaces())) {
6✔
551
        for (const addr of ifObj) {
24✔
552
            if (addr.family === 'IPv6') {
42✔
553
                if (!Server.notes.IPv6) Server.notes.IPv6 = true
24✔
554
            } else if (addr.family === 'IPv4') {
42✔
555
                if (!Server.notes.IPv4) Server.notes.IPv4 = true
18✔
556
            } else {
18!
557
                console.error(addr)
×
558
            }
×
559
        }
42✔
560
    }
24✔
561

6✔
562
    for (const listen_address of Server.get_listen_addrs(Server.cfg.main)) {
6✔
563
        const ep = endpoint(listen_address, 25)
6✔
564

6✔
565
        if (ep instanceof Error) {
6!
566
            Server.logerror(`Invalid "listen" format in smtp.ini: ${listen_address}`)
×
567
            continue
×
568
        }
×
569

6✔
570
        const server = await Server.get_smtp_server(ep, inactivity_timeout)
6✔
571
        if (!server) continue
6!
572

6✔
573
        server.notes = Server.notes
6✔
574
        if (Server.cluster) server.cluster = Server.cluster
6!
575

6✔
576
        server
6✔
577
            .on('listening', function () {
6✔
578
                const addr = this.address()
6✔
579
                Server.lognotice(`Listening on ${endpoint(addr)}`)
6✔
580
            })
6✔
581
            .on('close', () => {
6✔
582
                Server.loginfo(`Listener ${ep} stopped`)
6✔
583
            })
6✔
584
            .on('error', (e) => {
6✔
585
                errors.push(e)
×
586
                Server.logerror(`Failed to setup listeners: ${e.message}`)
×
587
                if (e.code !== 'EAFNOSUPPORT') {
×
588
                    Server.logerror(e)
×
589
                    return
×
590
                }
×
591
                // Fallback from IPv6 to IPv4 if not supported
×
592
                // But only if we supplied the default of [::0]:25
×
593
                if (/^::0/.test(ep.host) && Server.default_host) {
×
594
                    server.listen(ep.port, '0.0.0.0', 0)
×
595
                    return
×
596
                }
×
597
                // Pass error to callback
×
598
                Server.logerror(e)
×
599
            })
6✔
600

6✔
601
        await ep.bind(server, { backlog: 0 })
6✔
602
    }
6✔
603

6✔
604
    if (errors.length) {
6!
605
        for (const e of errors) {
×
606
            Server.logerror(`Failed to setup listeners: ${e.message}`)
×
607
        }
×
608
        return logger.dump_and_exit(-1)
×
609
    }
×
610
    Server.listening()
6✔
611
    plugins2.run_hooks(`init_${type}`, Server)
6✔
612
}
6✔
613

2✔
614
Server.setup_http_listeners = async () => {
2✔
615
    if (!Server.http?.cfg?.listen) return
6!
616

×
617
    const listeners = Server.get_listen_addrs(Server.http.cfg, 80)
×
618
    if (!listeners.length) return
×
619

×
620
    try {
×
621
        Server.http.express = require('express')
×
622
        Server.loginfo('express loaded at Server.http.express')
×
623
    } catch {
×
624
        Server.logerror('express failed to load. No http server. Install express with: npm install -g express')
×
625
        return
×
626
    }
×
627

×
628
    const app = Server.http.express()
×
629
    Server.http.app = app
×
630
    Server.loginfo('express app is at Server.http.app')
×
631

×
632
    for (const listen_address of listeners) {
×
633
        const ep = endpoint(listen_address, 80)
×
634
        if (ep instanceof Error) {
×
635
            Server.logerror(`Invalid format for listen in http.ini: ${listen_address}`)
×
636
            continue
×
637
        }
×
638

×
639
        if (443 == ep.port) {
×
640
            const tlsOpts = { ...tls_socket.certsByHost['*'] }
×
641
            tlsOpts.requestCert = false // not appropriate for HTTPS
×
NEW
642
            Server.http.server = require('node:https').createServer(tlsOpts, app)
×
643
        } else {
×
NEW
644
            Server.http.server = require('node:http').createServer(app)
×
645
        }
×
646

×
647
        Server.listeners.push(Server.http.server)
×
648

×
649
        Server.http.server.on('listening', function () {
×
650
            Server.lognotice(`Listening on ${endpoint(this.address())}`)
×
651
        })
×
652

×
653
        Server.http.server.on('error', (e) => {
×
654
            Server.logerror(e)
×
655
        })
×
656

×
657
        await ep.bind(Server.http.server, { backlog: 0 })
×
658
    }
×
659

×
660
    Server.plugins.run_hooks('init_http', Server)
×
661
    app.use(Server.http.express.static(Server.get_http_docroot()))
×
662
    app.use(Server.handle404)
×
663
}
6✔
664

2✔
665
Server.init_master_respond = async (retval, msg) => {
2✔
666
    if (!(retval === constants.ok || retval === constants.cont)) {
6!
667
        Server.logerror(`init_master returned error${msg ? `: ${msg}` : ''}`)
×
668
        return logger.dump_and_exit(1)
×
669
    }
×
670

6✔
671
    const c = Server.cfg.main
6✔
672
    Server.ready = 1
6✔
673

6✔
674
    // Load the queue if we're just one process
6✔
675
    if (!(cluster && c.nodes)) {
6✔
676
        try {
6✔
677
            await outbound.init_queue()
6✔
678
        } catch {
6!
679
            Server.logcrit('Loading queue failed. Shutting down.')
×
680
            return logger.dump_and_exit(1)
×
681
        }
×
682
        Server.setup_http_listeners()
6✔
683
        return
6✔
684
    }
6!
685

×
686
    // Running under cluster, fork children here, so that
×
687
    // cluster events can be registered in init_master hooks.
×
688
    try {
×
689
        const pids = await outbound.scan_queue_pids()
×
690
        Server.daemonize()
×
691
        // Fork workers
×
692
        const workers = c.nodes === 'cpus' ? os.cpus().length : c.nodes
6!
693
        const new_workers = []
6✔
694
        for (let i = 0; i < workers; i++) {
6!
695
            new_workers.push(cluster.fork({ CLUSTER_MASTER_PID: process.pid }))
×
696
        }
×
697
        for (let j = 0; j < pids.length; j++) {
×
698
            new_workers[j % new_workers.length].send({
×
699
                event: 'outbound.load_pid_queue',
×
700
                data: pids[j],
×
701
            })
×
702
        }
×
703
        cluster.on('online', (worker) => {
×
704
            Server.lognotice('worker started', {
×
705
                worker: worker.id,
×
706
                pid: worker.process.pid,
×
707
            })
×
708
        })
×
709
        cluster.on('listening', (worker, address) => {
×
710
            Server.lognotice(`worker ${worker.id} listening on ${endpoint(address)}`)
×
711
        })
×
712
        cluster.on('exit', cluster_exit_listener)
×
713
    } catch {
×
714
        Server.logcrit('Scanning queue failed. Shutting down.')
×
715
        logger.dump_and_exit(1)
×
716
    }
×
717
}
6✔
718

2✔
719
function cluster_exit_listener(worker, code, signal) {
×
720
    if (signal) {
×
721
        Server.lognotice(`worker ${worker.id} killed by signal ${signal}`)
×
722
    } else if (code !== 0) {
×
723
        Server.lognotice(`worker ${worker.id} exited with error code: ${code}`)
×
724
    }
×
725
    if (signal || code !== 0) {
×
726
        // Restart worker
×
727
        const new_worker = cluster.fork({
×
728
            CLUSTER_MASTER_PID: process.pid,
×
729
        })
×
730
        new_worker.send({
×
731
            event: 'outbound.load_pid_queue',
×
732
            data: worker.process.pid,
×
733
        })
×
734
    }
×
735
}
×
736

2✔
737
Server.init_child_respond = (retval, msg) => {
2✔
738
    switch (retval) {
2✔
739
        case constants.ok:
2✔
740
        case constants.cont:
2✔
741
            Server.setup_http_listeners()
1✔
742
            return
1✔
743
    }
2✔
744

1✔
745
    const pid = process.env.CLUSTER_MASTER_PID
1✔
746
    Server.logerror(`init_child returned error ${msg ? `: ${msg}` : ''}`)
2!
747
    try {
2✔
748
        if (pid) {
2✔
749
            process.kill(pid)
1✔
750
            Server.logerror(`Killing master (pid=${pid})`)
1✔
751
        }
1✔
752
    } catch {
2!
753
        Server.logerror('Terminating child')
×
754
    }
✔
755
    logger.dump_and_exit(1)
1✔
756
}
2✔
757

2✔
758
Server.listening = () => {
2✔
759
    const c = Server.cfg.main
7✔
760

7✔
761
    // Drop privileges
7✔
762
    if (c.group) {
7✔
763
        Server.lognotice(`Switching from current gid: ${process.getgid()}`)
1✔
764
        process.setgid(c.group)
1✔
765
        Server.lognotice(`New gid: ${process.getgid()}`)
1✔
766
    }
1✔
767
    if (c.user) {
7✔
768
        Server.lognotice(`Switching from current uid: ${process.getuid()}`)
1✔
769
        process.setuid(c.user)
1✔
770
        Server.lognotice(`New uid: ${process.getuid()}`)
1✔
771
    }
1✔
772

7✔
773
    Server.ready = 1
7✔
774
}
7✔
775

2✔
776
Server.init_http_respond = () => {
2✔
777
    Server.loginfo('init_http_respond')
1✔
778

1✔
779
    let WebSocketServer
1✔
780
    try {
1✔
781
        WebSocketServer = require('ws').Server
1✔
782
    } catch {
1✔
783
        Server.logerror(`unable to load ws.\n  did you: npm install -g ws?`)
1✔
784
        return
1✔
785
    }
1!
786

×
787
    if (!WebSocketServer) {
×
788
        Server.logerror('ws failed to load')
×
789
        return
×
790
    }
×
791

×
792
    Server.http.wss = new WebSocketServer({ server: Server.http.server })
×
793
    Server.loginfo('Server.http.wss loaded')
×
794

×
795
    Server.plugins.run_hooks('init_wss', Server)
×
796
}
1✔
797

2✔
798
Server.init_wss_respond = () => {
2✔
799
    Server.loginfo('init_wss_respond')
×
800
}
×
801

2✔
802
Server.get_http_docroot = () => {
2✔
803
    if (Server.http.cfg.docroot) return Server.http.cfg.docroot
4✔
804

1✔
805
    Server.http.cfg.docroot = path.join(process.env.HARAKA || __dirname, 'http', 'html')
4✔
806
    Server.loginfo(`using html docroot: ${Server.http.cfg.docroot}`)
4✔
807
    return Server.http.cfg.docroot
4✔
808
}
4✔
809

2✔
810
Server.handle404 = (req, res) => {
2✔
811
    // abandon all hope, serve up a 404
3✔
812
    const docroot = Server.get_http_docroot()
3✔
813

3✔
814
    // respond with html page
3✔
815
    if (req.accepts('html')) {
3✔
816
        res.status(404).sendFile('404.html', { root: docroot })
1✔
817
        return
1✔
818
    }
1✔
819

2✔
820
    // respond with json
2✔
821
    if (req.accepts('json')) {
3✔
822
        res.status(404).send({ err: 'Not found' })
1✔
823
        return
1✔
824
    }
1✔
825

1✔
826
    res.status(404).send('Not found!')
1✔
827
}
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc