Writing Haraka Plugins

May 13, 2026 · View on GitHub

Part of the joy of using Haraka as your main mail server is having a strong plugin system: you control every aspect of how mail is processed, accepted, and delivered.

This tutorial walks through a small plugin that supports disposable addresses: an email like user-20271231@example.com is accepted up until 31 December 2027, after which delivery is rejected. Mail that is still within the validity window is rewritten back to user@example.com before it is forwarded on.

What You'll Need

  • Node.js (an active LTS release) and npm
  • Haraka
  • A text editor
  • swaks for sending test mail

Getting Started

Install Haraka and create a project:

sudo npm install -g Haraka
haraka -i /path/to/new_project

Use a directory that does not yet exist. Now scaffold a plugin:

haraka -c /path/to/new_project -p rcpt_to.disposable

haraka -p reports the files it created:

Plugin rcpt_to.disposable created
Now edit javascript in:    /path/to/new_project/plugins/rcpt_to.disposable.js
Add the plugin to config:  /path/to/new_project/config/plugins
And edit documentation in: /path/to/new_project/docs/plugins/rcpt_to.disposable.md

Edit config/plugins so the only enabled lines are:

rcpt_to.disposable
rcpt_to.in_host_list
queue/test

The ordering matters — the disposable plugin must run before rcpt_to.in_host_list, which accepts mail for domains listed in config/host_list. queue/test writes accepted mail to a .eml file in os.tmpdir() so you can confirm delivery.

Open plugins/rcpt_to.disposable.js and start with:

exports.hook_rcpt = (next, connection, params) => {
    const rcpt = params[0]
    connection.loginfo(`got recipient: ${rcpt}`)
    next()
}

Verify it works. In one terminal:

echo LOGDEBUG > config/loglevel
echo myserver.com >> config/host_list
sudo haraka -c /path/to/new_project

In another:

swaks -h example.com -t booya@myserver.com -f sender@example.com -s localhost -p 25

You should see something like this in the Haraka log:

[INFO] [<uuid>] [rcpt_to.disposable] got recipient: <booya@myserver.com>

…and a .eml file in your system temp directory containing the message.

Parsing Out the Date

Detect addresses of the form user-YYYYMMDD and parse the date:

exports.hook_rcpt = (next, connection, params) => {
    const rcpt = params[0]
    connection.loginfo(`got recipient: ${rcpt}`)

    const match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user)
    if (!match) return next()

    // Date constructor uses zero-indexed months (Dec === 11)
    const expiry = new Date(match[2], match[3] - 1, match[4])
    connection.loginfo(`expires on: ${expiry.toISOString()}`)

    next()
}

Restart Haraka and send:

swaks -h example.com -t booya-20271231@myserver.com \
    -f sender@example.com -s localhost -p 25

Logs:

[INFO] [rcpt_to.disposable] got recipient: <booya-20271231@myserver.com>
[INFO] [rcpt_to.disposable] expires on: 2027-12-31T00:00:00.000Z

Rejecting Expired Addresses

Compare the parsed date to today and reject if it has already passed:

exports.hook_rcpt = (next, connection, params) => {
    const rcpt = params[0]

    const match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user)
    if (!match) return next()

    const expiry = new Date(match[2], match[3] - 1, match[4])
    if (expiry < new Date()) {
        return next(DENY, 'Expired email address')
    }

    next()
}

Send mail to an expired address:

swaks -h example.com -t booya-20200101@myserver.com \
    -f sender@example.com -s localhost -p 25

The remote end sees:

<** 550 Expired email address

Rewriting Live Addresses

When the address is still valid, strip the date tag so the downstream mail store receives plain user@domain:

exports.hook_rcpt = (next, connection, params) => {
    const rcpt = params[0]

    const match = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(rcpt.user)
    if (!match) return next()

    const expiry = new Date(match[2], match[3] - 1, match[4])
    if (expiry < new Date()) {
        return next(DENY, 'Expired email address')
    }

    rcpt.user = match[1]
    connection.loginfo(`rewrote recipient to: ${rcpt}`)
    next()
}

Send to a live tagged address and watch the log:

[INFO] [rcpt_to.disposable] rewrote recipient to: <booya@myserver.com>

Further Reading

The Haraka API offers much more — body and header access, ESMTP extension hooks, the outbound delivery hooks, structured results, attachments, and so on. Two good starting points:

  • The Plugins guide and the rest of the docs/ directory.
  • The Plugin registry for an inventory of real-world plugins.
  • The plugins shipped in the plugins/ directory. Even the most elaborate are under 200 lines; many are under 20.