Repository: haraka/Haraka
Pull request #3358 (2024-05-12 21:01:29)

style: more async/await
Changes proposed in this pull request:

- es6(async/promise): pre_send_trans_email_respond, process_delivery
- server:
  - es6(async) _graceful, get_smtp_server, setup_smtp_listeners
  - replace async.eachLimit with Promise.all batches
  - status: replace async.map with Promise.allSettled

Checklist:
- [ ] docs updated
- [x] tests updated
- [x] [Changes](https://github.com/haraka/Haraka/blob/master/Changes.md) updated



Patch:
diff --git a/Changes.md b/Changes.md
index 819b6362..e3f0ed13 100644
--- a/Changes.md
+++ b/Changes.md
@@ -58,6 +58,7 @@
   - remove config setting ipv6_enabled #3322
   - remove undocumented use of send_email with arity of 2. #3322
   - encapsulate force_tls logic into get_force_tls #3322
+  - es6(async/promise): pre_send_trans_email_respond, process_delivery
 - queue/lmtp: refactored for DRY and improved readability #3322
 - smtp_client: pass connect_timeout, maybe fixes #3281
 - spamassassin: repackaged as NPM module #3348
@@ -66,6 +67,9 @@
 - doc(Plugins.md): update registry
 - remove last vestiges of header_hide_version (long ago renamed)
 - server.js: use the local logger methods
+  - es6(async): _graceful, get_smtp_server, setup_smtp_listeners
+  - replace async.eachLimit with Promise.all batches
+- status: replace async.map with Promise.allSettled
 - get Haraka version from utils.getVersion (which includes git id if running from repo)
 - tls_socket: remove secureConnection. Fixes #2743
   - getSocketOpts is now async
diff --git a/outbound/hmail.js b/outbound/hmail.js
index 7a720549..6afe368d 100644
--- a/outbound/hmail.js
+++ b/outbound/hmail.js
@@ -262,7 +262,7 @@ class HMailItem extends events.EventEmitter {
             return this.bounce(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`);
         }
 
-        // resolves the MX hostnames into IPs
+        // resolves the MX hostnames to IPs
         this.mxlist = await net_utils.resolve_mx_hosts(mxs);
 
         this.try_deliver();
diff --git a/outbound/index.js b/outbound/index.js
index d2410ece..5297cc24 100644
--- a/outbound/index.js
+++ b/outbound/index.js
@@ -3,7 +3,6 @@
 const fs          = require('node:fs');
 const path        = require('node:path');
 
-const async       = require('async');
 const { Address } = require('address-rfc2821');
 const config      = require('haraka-config');
 const constants   = require('haraka-constants');
@@ -232,75 +231,76 @@ exports.send_trans_email = function (transaction, next) {
         transaction.results = new ResultStore(connection);
     }
 
-    connection.pre_send_trans_email_respond = retval => {
+    connection.pre_send_trans_email_respond = async (retval) => {
         const deliveries = get_deliveries(transaction);
         const hmails = [];
         const ok_paths = [];
 
         let todo_index = 1;
 
-        async.forEachSeries(deliveries, (deliv, cb) => {
-            const todo = new TODOItem(deliv.domain, deliv.rcpts, transaction);
-            todo.uuid = `${todo.uuid}.${todo_index}`;
-            todo_index++;
-            this.process_delivery(ok_paths, todo, hmails, cb);
-        },
-        (err) => {
-            if (err) {
-                for (let i=0, l=ok_paths.length; i<l; i++) {
-                    fs.unlink(ok_paths[i], () => {});
-                }
-                transaction.results.add({ name: 'outbound'}, { err });
-                if (next) next(constants.denysoft, err);
-                return;
+        try {
+            for (const deliv of deliveries) {
+                const todo = new TODOItem(deliv.domain, deliv.rcpts, transaction);
+                todo.uuid = `${todo.uuid}.${todo_index}`;
+                todo_index++;
+                await this.process_delivery(ok_paths, todo, hmails);
             }
-
-            for (const hmail of hmails) {
-                delivery_queue.push(hmail);
+        }
+        catch (err) {
+            for (let i=0, l=ok_paths.length; i<l; i++) {
+                fs.unlink(ok_paths[i], () => {});
             }
+            transaction.results.add({ name: 'outbound'}, { err });
+            if (next) next(constants.denysoft, err);
+            return;
+        }
 
-            transaction.results.add({ name: 'outbound'}, { pass: "queued" });
-            if (next) {
-                next(constants.ok, `Message Queued (${transaction.uuid})`);
-            }
-        });
+        for (const hmail of hmails) {
+            delivery_queue.push(hmail);
+        }
+
+        transaction.results.add({ name: 'outbound'}, { pass: "queued" });
+        if (next) next(constants.ok, `Message Queued (${transaction.uuid})`);
     }
 
     plugins.run_hooks('pre_send_trans_email', connection);
 }
 
-exports.process_delivery = function (ok_paths, todo, hmails, cb) {
-    logger.info(exports, `Transaction delivery for domain: ${todo.domain}`);
-    const fname = _qfile.name();
-    const tmp_path = path.join(queue_dir, `${_qfile.platformDOT}${fname}`);
-    const ws = new FsyncWriteStream(tmp_path, { flags: constants.WRITE_EXCL });
+exports.process_delivery = function (ok_paths, todo, hmails) {
+    return new Promise((resolve, reject) => {
+
+        logger.info(exports, `Transaction delivery for domain: ${todo.domain}`);
+        const fname = _qfile.name();
+        const tmp_path = path.join(queue_dir, `${_qfile.platformDOT}${fname}`);
+        const ws = new FsyncWriteStream(tmp_path, { flags: constants.WRITE_EXCL });
+
+        ws.on('close', () => {
+            const dest_path = path.join(queue_dir, fname);
+            fs.rename(tmp_path, dest_path, err => {
+                if (err) {
+                    logger.error(exports, `Unable to rename tmp file!: ${err}`);
+                    fs.unlink(tmp_path, () => {});
+                    reject("Queue error");
+                }
+                else {
+                    hmails.push(new HMailItem (fname, dest_path, todo.notes));
+                    ok_paths.push(dest_path);
+                    resolve();
+                }
+            })
+        })
 
-    ws.on('close', () => {
-        const dest_path = path.join(queue_dir, fname);
-        fs.rename(tmp_path, dest_path, err => {
-            if (err) {
-                logger.error(exports, `Unable to rename tmp file!: ${err}`);
-                fs.unlink(tmp_path, () => {});
-                cb("Queue error");
-            }
-            else {
-                hmails.push(new HMailItem (fname, dest_path, todo.notes));
-                ok_paths.push(dest_path);
-                cb();
-            }
+        ws.on('error', err => {
+            logger.error(exports, `Unable to write queue file (${fname}): ${err}`);
+            ws.destroy();
+            fs.unlink(tmp_path, () => {});
+            reject("Queueing failed");
         })
-    })
 
-    ws.on('error', err => {
-        logger.error(exports, `Unable to write queue file (${fname}): ${err}`);
-        ws.destroy();
-        fs.unlink(tmp_path, () => {});
-        cb("Queueing failed");
+        this.build_todo(todo, ws, () => {
+            todo.message_stream.pipe(ws, { dot_stuffing: true });
+        });
     })
-
-    this.build_todo(todo, ws, () => {
-        todo.message_stream.pipe(ws, { dot_stuffing: true });
-    });
 }
 
 exports.build_todo = (todo, ws, write_more) => {
diff --git a/outbound/queue.js b/outbound/queue.js
index fee9669e..eec33878 100644
--- a/outbound/queue.js
+++ b/outbound/queue.js
@@ -86,16 +86,15 @@ exports.load_queue = pid => {
 }
 
 exports._load_cur_queue = (pid, iteratee, cb) => {
-    const self = exports;
     logger.info(exports, "Loading outbound queue from ", queue_dir);
     fs.readdir(queue_dir, (err, files) => {
         if (err) {
             return logger.error(exports, `Failed to load queue directory (${queue_dir}): ${err}`);
         }
 
-        self.cur_time = new Date(); // set once so we're not calling it a lot
+        this.cur_time = new Date(); // set once so we're not calling it a lot
 
-        self.load_queue_files(pid, files, iteratee, cb);
+        this.load_queue_files(pid, files, iteratee, cb);
     });
 }
 
diff --git a/outbound/timer_queue.js b/outbound/timer_queue.js
index 2691089a..46d9c55c 100644
--- a/outbound/timer_queue.js
+++ b/outbound/timer_queue.js
@@ -19,6 +19,7 @@ class TQTimer {
 class TimerQueue {
 
     constructor (interval = 1000) {
+        this.name = 'outbound/timer_queue'
         this.queue = [];
         this.interval_timer = setInterval(() => { this.fire(); }, interval);
         this.interval_timer.unref() // allow server to exit
@@ -72,7 +73,7 @@ class TimerQueue {
     }
 
     drain () {
-        logger.debug({ name: 'outbound/timer_queue'}, `Draining ${this.queue.length} items from the queue`);
+        logger.debug(this, `Draining ${this.queue.length} items from the queue`);
         while (this.queue.length) {
             const to_run = this.queue.shift();
             if (to_run.cb) to_run.cb();
diff --git a/plugins/status.js b/plugins/status.js
index 39b7e3b5..b77eb89b 100644
--- a/plugins/status.js
+++ b/plugins/status.js
@@ -3,8 +3,6 @@
 const fs = require('node:fs');
 const path = require('node:path');
 
-const async = require('async');
-
 exports.register = function () {
     this.outbound = require('../outbound');
     this.queue_dir = require('../outbound/queue').queue_dir;
@@ -82,38 +80,36 @@ exports.pool_list = cb => {
     const result = {};
 
     if (server.notes.pool) {
-        Object.keys(server.notes.pool).forEach(name => {
+        for (const name of Object.keys(server.notes.pool)) {
             const instance = server.notes.pool[name];
 
             result[name] = {
                 inUse: instance.inUseObjectsCount(),
                 size: instance.getPoolSize()
             };
-        });
+        }
     }
 
     cb(null, result);
 }
 
 exports.queue_list = function (cb) {
-    this.outbound.list_queue((err, qlist) => {
-        if (err) cb(err);
-
+    this.outbound.list_queue((err, qlist = []) => {
         const result = [];
 
-        if (qlist) {
-            qlist.forEach((todo) => result.push({
+        for (const todo of qlist) {
+            result.push({
                 file: todo.file,
                 uuid: todo.uuid,
                 queue_time: todo.queue_time,
                 domain: todo.domain,
                 from: todo.mail_from.toString(),
                 to: todo.rcpt_to.map((r) => r.toString())
-            }));
+            })
         }
 
         cb(err, result);
-    });
+    })
 }
 
 exports.queue_stats = function (cb) {
@@ -166,14 +162,14 @@ exports.queue_push = function (file, cb) {
 // cluster IPC
 
 exports.hook_init_master = function (next) {
-    const self = this;
+    const plugin = this;
 
     if (!server.cluster) return next();
 
     function message_handler (sender, msg) {
         if (msg.event !== 'status.request') return;
 
-        self.call_workers(msg, (err, response) => {
+        plugin.call_workers(msg, (response) => {
             msg.result = response.filter((el) => el != null);
             msg.event = 'status.result';
             sender.send(msg);
@@ -215,32 +211,39 @@ exports.call_master = (cmd, cb) => {
 }
 
 exports.call_workers = function (cmd, cb) {
-
-    async.map(server.cluster.workers, (w, done) => {
-        this.call_worker(w, cmd, done);
-    }, cb);
+    Promise.allSettled(
+        Object.values(server.cluster.workers).map(w => this.call_worker(w, cmd))
+    )
+    .then(r => {
+        cb(
+            // r.filter(s => s.status === 'rejected').flatMap(s => s.reason),
+            r.filter(s => s.status === 'fulfilled').flatMap(s => s.value),
+        )
+    })
 }
 
 // sends command to worker and then wait for response or timeout
-exports.call_worker = (worker, cmd, cb) => {
-    let timeout;
+exports.call_worker = (worker, cmd) => {
+    return new Promise((resolve) => {
+        let timeout;
 
-    function message_handler (sender, msg) {
-        if (sender.id !== worker.id) return;
-        if (msg.event !== 'status.response') return;
+        function message_handler (sender, msg) {
+            if (sender.id !== worker.id) return;
+            if (msg.event !== 'status.response') return;
 
-        clearTimeout(timeout);
-        server.cluster.removeListener('message', message_handler);
+            clearTimeout(timeout);
+            server.cluster.removeListener('message', message_handler);
 
-        cb(null, msg.result);
-    }
+            resolve(msg.result);
+        }
 
-    timeout = setTimeout(() => {
-        server.cluster.removeListener('message', message_handler);
-        cb();
-    }, 1000);
+        timeout = setTimeout(() => {
+            server.cluster.removeListener('message', message_handler);
+            resolve();
+        }, 1000);
 
 
-    server.cluster.on('message', message_handler);
-    worker.send(cmd);
+        server.cluster.on('message', message_handler);
+        worker.send(cmd);
+    })
 }
diff --git a/server.js b/server.js
index ccda7140..68ba3fe7 100644
--- a/server.js
+++ b/server.js
@@ -7,13 +7,13 @@ const os          = require('node:os');
 const path        = require('node:path');
 const tls         = require('node:tls');
 
-const async       = require('async');
 const daemon      = require('daemon');
+const constants   = require('haraka-constants');
+
 const tls_socket  = require('./tls_socket');
 const conn        = require('./connection');
 const outbound    = require('./outbound');
 const endpoint    = require('./endpoint');
-const constants   = require('haraka-constants');
 
 const Server      = exports;
 Server.logger     = require('./logger');
@@ -148,7 +148,7 @@ Server.gracefulShutdown = () => {
     });
 }
 
-Server._graceful = shutdown => {
+Server._graceful = async (shutdown) => {
     if (!Server.cluster && shutdown) {
         for (const module of ['outbound', 'cfreader', 'plugins']) {
             process.emit('message', {event: `${module}.shutdown`});
@@ -167,66 +167,80 @@ Server._graceful = shutdown => {
     const disconnect_timeout = 30;
     const exit_timeout = 30;
     cluster.removeAllListeners('exit');
+
     // we reload using eachLimit where limit = num_workers - 1
     // this kills all-but-one workers in parallel, leaving one running
     // for new connections, and then restarts that one last worker.
+
     const worker_ids = Object.keys(cluster.workers);
     let limit = worker_ids.length - 1;
     if (limit < 2) limit = 1;
-    async.eachLimit(worker_ids, limit, (id, cb) => {
-        Server.lognotice(`Killing node: ${id}`);
-        const worker = cluster.workers[id];
-        for (const module of ['outbound', 'cfreader', 'plugins']) {
-            worker.send({event: `${module  }.shutdown`});
-        }
-        worker.disconnect();
-        let disconnect_received = false;
-        const disconnect_timer = setTimeout(() => {
-            if (!disconnect_received) {
-                Server.logcrit("Disconnect never received by worker. Killing.");
-                worker.kill();
-            }
-        }, disconnect_timeout * 1000);
-        worker.once("disconnect", () => {
-            clearTimeout(disconnect_timer);
-            disconnect_received = true;
-            Server.lognotice("Disconnect complete");
-            let dead = false;
-            const timer = setTimeout(() => {
-                if (!dead) {
-                    Server.logcrit(`Worker ${id} failed to shutdown. Killing.`);
-                    worker.kill();
+
+    const todo = []
+
+    for (const id of Object.keys(cluster.workers)) {
+        todo.push((id) => {
+            return new Promise((resolve) => {
+                Server.lognotice(`Killing worker: ${id}`);
+                const worker = cluster.workers[id];
+                for (const module of ['outbound', 'cfreader', 'plugins']) {
+                    worker.send({event: `${module  }.shutdown`});
                 }
-            }, exit_timeout * 1000);
-            worker.once("exit", () => {
-                dead = true;
-                clearTimeout(timer);
-                if (shutdown) cb();
-            });
-        });
-        if (shutdown) return;
-        const newWorker = cluster.fork();
-        newWorker.once("listening", () => {
-            Server.lognotice("Replacement worker online.");
-            newWorker.on('exit', (code, signal) => {
-                cluster_exit_listener(newWorker, code, signal);
-            });
-            cb();
-        });
-    }, err => {
-        // err can basically never happen, but fuckit...
-        if (err) Server.logerror(err);
-        if (shutdown) {
-            Server.loginfo("Workers closed. Shutting down master process subsystems");
-            for (const module of ['outbound', 'cfreader', 'plugins']) {
-                process.emit('message', {event: `${module}.shutdown`});
-            }
-            const t2 = setTimeout(shutdown, Server.cfg.main.force_shutdown_timeout * 1000);
-            return t2.unref();
+                worker.disconnect();
+                let disconnect_received = false;
+                const disconnect_timer = setTimeout(() => {
+                    if (!disconnect_received) {
+                        Server.logcrit("Disconnect never received by worker. Killing.");
+                        worker.kill();
+                    }
+                }, disconnect_timeout * 1000);
+
+                worker.once('disconnect', () => {
+                    clearTimeout(disconnect_timer);
+                    disconnect_received = true;
+                    Server.lognotice('Disconnect complete');
+                    let dead = false;
+                    const timer = setTimeout(() => {
+                        if (!dead) {
+                            Server.logcrit(`Worker ${id} failed to shutdown. Killing.`);
+                            worker.kill();
+                        }
+                    }, exit_timeout * 1000);
+                    worker.once('exit', () => {
+                        dead = true;
+                        clearTimeout(timer);
+                        if (shutdown) resolve()
+                    })
+                })
+                if (!shutdown) {
+                    const newWorker = cluster.fork();
+                    newWorker.once('listening', () => {
+                        Server.lognotice('Replacement worker online.');
+                        newWorker.on('exit', (code, signal) => {
+                            cluster_exit_listener(newWorker, code, signal);
+                        });
+                        resolve()
+                    })
+                }
+            })
+        })
+    }
+
+    while (todo.length) {
+        // process batches of workers
+        await Promise.all(todo.splice(0, limit))
+    }
+
+    if (shutdown) {
+        Server.loginfo("Workers closed. Shutting down master process subsystems");
+        for (const module of ['outbound', 'cfreader', 'plugins']) {
+            process.emit('message', {event: `${module}.shutdown`});
         }
-        graceful_in_progress = false;
-        Server.lognotice(`Reload complete, workers: ${JSON.stringify(Object.keys(cluster.workers))}`);
-    });
+        const t2 = setTimeout(shutdown, Server.cfg.main.force_shutdown_timeout * 1000);
+        return t2.unref();
+    }
+    graceful_in_progress = false;
+    Server.lognotice(`Reload complete, workers: ${JSON.stringify(Object.keys(cluster.workers))}`);
 }
 
 Server.sendToMaster = (command, params) => {
@@ -342,7 +356,7 @@ Server.load_default_tls_config = done => {
     })
 }
 
-Server.get_smtp_server = (ep, inactivity_timeout, done) => {
+Server.get_smtp_server = async (ep, inactivity_timeout) => {
     let server;
 
     function onConnect (client) {
@@ -364,86 +378,89 @@ Server.get_smtp_server = (ep, inactivity_timeout, done) => {
 
     if (ep.port === parseInt(Server.cfg.main.smtps_port, 10)) {
         Server.loginfo('getting SocketOpts for SMTPS server');
-        tls_socket.getSocketOpts('*').then(opts => {
-            Server.loginfo(`Creating TLS server on ${ep}`);
+        const opts = await tls_socket.getSocketOpts('*')
+        Server.loginfo(`Creating TLS server on ${ep}`);
 
-            opts.rejectUnauthorized = tls_socket.get_rejectUnauthorized(opts.rejectUnauthorized, ep.port, tls_socket.cfg.main.requireAuthorized)
+        opts.rejectUnauthorized = tls_socket.get_rejectUnauthorized(opts.rejectUnauthorized, ep.port, tls_socket.cfg.main.requireAuthorized)
 
-            server = tls.createServer(opts, onConnect);
-            tls_socket.addOCSP(server);
-            server.has_tls=true;
-            server.on('resumeSession', (id, rsDone) => {
-                Server.loginfo('client requested TLS resumeSession');
-                rsDone(null, null);
-            })
-            Server.listeners.push(server);
-            done(server);
+        server = tls.createServer(opts, onConnect);
+        tls_socket.addOCSP(server);
+        server.has_tls=true;
+        server.on('resumeSession', (id, rsDone) => {
+            Server.loginfo('client requested TLS resumeSession');
+            rsDone(null, null);
         })
+        Server.listeners.push(server);
+        return server
     }
     else {
         server = tls_socket.createServer(onConnect);
         server.has_tls = false;
-        tls_socket.getSocketOpts('*').then(opts => {
-            Server.listeners.push(server);
-            done(server);
-        })
+        const opts = await tls_socket.getSocketOpts('*')
+        Server.listeners.push(server);
+        return server
     }
 }
 
-Server.setup_smtp_listeners = (plugins2, type, inactivity_timeout) => {
+Server.setup_smtp_listeners = async (plugins2, type, inactivity_timeout) => {
 
-    async.each(
-        Server.get_listen_addrs(Server.cfg.main),  // array of listeners
+    const errors = []
 
-        function setupListener (listen_address, listenerDone) {
+    for (const listen_address of Server.get_listen_addrs(Server.cfg.main)) {
 
-            const ep = endpoint(listen_address, 25);
-            if (ep instanceof Error) return listenerDone(
-                new Error(`Invalid "listen" format in smtp.ini: ${listen_address}`));
+        const ep = endpoint(listen_address, 25);
 
-            Server.get_smtp_server(ep, inactivity_timeout, (server) => {
-                if (!server) return listenerDone();
+        if (ep instanceof Error) {
+            Server.logerror(`Invalid "listen" format in smtp.ini: ${listen_address}`)
+            continue
+        }
 
-                server.notes = Server.notes;
-                if (Server.cluster) server.cluster = Server.cluster;
+        const server = await Server.get_smtp_server(ep, inactivity_timeout)
+        if (!server) continue;
 
-                server
-                    .on('listening', function () {
-                        const addr = this.address();
-                        Server.lognotice(`Listening on ${endpoint(addr)}`);
-                        listenerDone();
-                    })
-                    .on('close', () => {
-                        Server.loginfo(`Listener ${ep} stopped`);
-                    })
-                    .on('error', e => {
-                        if (e.code !== 'EAFNOSUPPORT') return listenerDone(e);
-                        // Fallback from IPv6 to IPv4 if not supported
-                        // But only if we supplied the default of [::0]:25
-                        if (/^::0/.test(ep.host) && Server.default_host) {
-                            server.listen(ep.port, '0.0.0.0', 0);
-                            return;
-                        }
-                        // Pass error to callback
-                        listenerDone(e);
-                    });
-                ep.bind(server, {backlog: 0});
-            });
-        },
-        function runInitHooks (err) {
-            if (err) {
-                Server.logerror(`Failed to setup listeners: ${err.message}`);
-                return logger.dump_and_exit(-1);
-            }
-            Server.listening();
-            plugins2.run_hooks(`init_${type}`, Server);
+        server.notes = Server.notes;
+        if (Server.cluster) server.cluster = Server.cluster;
+
+        server
+            .on('listening', function () {
+                const addr = this.address();
+                Server.lognotice(`Listening on ${endpoint(addr)}`);
+            })
+            .on('close', () => {
+                Server.loginfo(`Listener ${ep} stopped`);
+            })
+            .on('error', e => {
+                errors.push(e)
+                Server.logerror(`Failed to setup listeners: ${e.message}`);
+                if (e.code !== 'EAFNOSUPPORT') {
+                    Server.logerror(e)
+                    return
+                }
+                // Fallback from IPv6 to IPv4 if not supported
+                // But only if we supplied the default of [::0]:25
+                if (/^::0/.test(ep.host) && Server.default_host) {
+                    server.listen(ep.port, '0.0.0.0', 0);
+                    return;
+                }
+                // Pass error to callback
+                Server.logerror(e)
+            })
+
+        ep.bind(server, {backlog: 0});
+    }
+
+    if (errors.length) {
+        for (const e of errors) {
+            Server.logerror(`Failed to setup listeners: ${e.message}`);
         }
-    );
+        return logger.dump_and_exit(-1);
+    }
+    Server.listening();
+    plugins2.run_hooks(`init_${type}`, Server);
 }
 
 Server.setup_http_listeners = () => {
-    if (!Server.http.cfg) return;
-    if (!Server.http.cfg.listen) return;
+    if (!Server.http?.cfg?.listen) return;
 
     const listeners = Server.get_listen_addrs(Server.http.cfg, 80);
     if (!listeners.length) return;
@@ -453,8 +470,7 @@ Server.setup_http_listeners = () => {
         Server.loginfo('express loaded at Server.http.express');
     }
     catch (err) {
-        Server.logerror('express failed to load. No http server. ' +
-                ' Try installing express with: npm install -g express');
+        Server.logerror('express failed to load. No http server. Install express with: npm install -g express');
         return;
     }
 
@@ -462,17 +478,17 @@ Server.setup_http_listeners = () => {
     Server.http.app = app;
     Server.loginfo('express app is at Server.http.app');
 
-    function setupListener (listen_address, cb) {
+    for (const listen_address of listeners) {
+
         const ep = endpoint(listen_address, 80);
         if (ep instanceof Error) {
-            return cb(new Error(`Invalid format for listen in http.ini: ${listen_address}`));
+            Server.logerror(`Invalid format for listen in http.ini: ${listen_address}`)
+            continue
         }
 
         if (443 == ep.port) {
-            // clone the default TLS opts
-            const tlsOpts = Object.assign({}, tls_socket.certsByHost['*']);
+            const tlsOpts = { ...tls_socket.certsByHost['*'] }
             tlsOpts.requestCert = false; // not appropriate for HTTPS
-            // console.log(tlsOpts);
             Server.http.server = require('https').createServer(tlsOpts, app);
         }
         else {
@@ -482,30 +498,19 @@ Server.setup_http_listeners = () => {
         Server.listeners.push(Server.http.server);
 
         Server.http.server.on('listening', function () {
-            const addr = this.address();
-            Server.lognotice(`Listening on ${endpoint(addr)}`);
-            cb();
-        });
+            Server.lognotice(`Listening on ${endpoint(this.address())}`);
+        })
 
         Server.http.server.on('error', e => {
             Server.logerror(e);
-            cb(e);
-        });
+        })
 
         ep.bind(Server.http.server, {backlog: 0});
     }
 
-    function registerRoutes (err) {
-        if (err) {
-            Server.logerror(`Failed to setup http routes: ${err.message}`);
-        }
-
-        Server.plugins.run_hooks('init_http', Server);
-        app.use(Server.http.express.static(Server.get_http_docroot()));
-        app.use(Server.handle404);
-    }
-
-    async.each(listeners, setupListener, registerRoutes);
+    Server.plugins.run_hooks('init_http', Server);
+    app.use(Server.http.express.static(Server.get_http_docroot()));
+    app.use(Server.handle404);
 }
 
 Server.init_master_respond = (retval, msg) => {
@@ -636,13 +641,12 @@ Server.init_http_respond = () => {
 
 Server.init_wss_respond = () => {
     Server.loginfo('init_wss_respond');
-    // Server.logdebug(arguments);
 }
 
 Server.get_http_docroot = () => {
     if (Server.http.cfg.docroot) return Server.http.cfg.docroot;
 
-    Server.http.cfg.docroot = path.join( (process.env.HARAKA || __dirname), '/html');
+    Server.http.cfg.docroot = path.join( (process.env.HARAKA || __dirname), 'http', 'html');
     Server.loginfo(`using html docroot: ${Server.http.cfg.docroot}`);
     return Server.http.cfg.docroot;
 }
diff --git a/tls_socket.js b/tls_socket.js
index 5876f2b8..a4e04b2a 100644
--- a/tls_socket.js
+++ b/tls_socket.js
@@ -660,7 +660,6 @@ function getCertFor (host) {
     return certsByHost['*'];  // the default TLS cert
 }
 
-
 function connect (conn_options = {}) {
     // called by outbound/client_pool, smtp_client, plugins/spamassassin,avg,clamd
 


Test patch:
diff --git a/test/fixtures/util_hmailitem.js b/test/fixtures/util_hmailitem.js
index e3237bb3..ddc975e4 100644
--- a/test/fixtures/util_hmailitem.js
+++ b/test/fixtures/util_hmailitem.js
@@ -79,11 +79,7 @@ exports.createHMailItem = (outbound_context, options, callback) => {
 
     const hmails = [];
     const ok_paths = [];
-    outbound_context.exports.process_delivery(ok_paths, todo, hmails, err => {
-        if (err) {
-            callback(`process_delivery error: ${err}`);
-            return;
-        }
+    outbound_context.exports.process_delivery(ok_paths, todo, hmails).then(() => {
         if (hmails.length == 0) {
             callback('No hmail producted');
             return;
@@ -92,8 +88,10 @@ exports.createHMailItem = (outbound_context, options, callback) => {
             hmail.hostlist = [ delivery_domain ];
             callback(null, hmail);
         }
-    });
-
+    })
+    .catch(err => {
+        callback(`process_delivery error: ${err}`);
+    })
 }
 
 /**
diff --git a/test/server.js b/test/server.js
index ed199073..a2039457 100644
--- a/test/server.js
+++ b/test/server.js
@@ -135,7 +135,7 @@ describe('server', () => {
         })
 
         it('gets a net server object', (done) => {
-            this.server.get_smtp_server(endpoint('0.0.0.0:2501'), 10, (server) => {
+            this.server.get_smtp_server(endpoint('0.0.0.0:2501'), 10).then(server => {
                 if (!server) {
                     console.error('unable to bind to 0.0.0.0:2501');
                     if (process.env.CI) return // can't bind to IP/port (fails on Travis)
@@ -151,7 +151,7 @@ describe('server', () => {
 
         it('gets a TLS net server object', (done) => {
             this.server.cfg.main.smtps_port = 2502;
-            this.server.get_smtp_server(endpoint('0.0.0.0:2502'), 10, (server) => {
+            this.server.get_smtp_server(endpoint('0.0.0.0:2502'), 10).then((server) => {
                 if (!server) {
                     console.error('unable to bind to 0.0.0.0:2502');
                     if (process.env.CI) return // can't bind to IP/port (fails on Travis)