Outbound Mail with Haraka

May 24, 2026 · View on GitHub

A default Haraka installation queues outbound mail to disk and delivers it to the appropriate MX for each recipient domain. Temporary failures are retried automatically using the configured backoff schedule.

A mail is treated as outbound when a plugin sets connection.relaying to true. The simplest way is SMTP AUTH using auth/flat_file or one of the auth plugins; the relay plugin offers allow-list-based variants, and a custom plugin can apply any policy.

For live stats on the outbound queue see the process_title plugin.

To flush the temp-fail queue (e.g. after fixing network or DNS), send SIGHUP to the Haraka master process.

Outbound Configuration

outbound.ini

KeyDefaultDescription
disabledfalsePause outbound delivery while still queuing inbound mail. Reloadable at runtime.
concurrency_max10000Maximum concurrent outbound deliveries per worker. Effective total is concurrency_max × nodes.
enable_tlstrueUse opportunistic STARTTLS on outbound.
maxTempFailures13Maximum temp-fail retries before the message bounces. Ignored if temp_fail_intervals is set.
temp_fail_intervalsderivedComma-separated <n><unit>[*<count>] pattern. 1m, 5m*2, 1h*3[60,300,300,3600,3600,3600] seconds. none bounces on first temp-fail.
always_splitfalseCreate one queue file per recipient (instead of one per destination domain). Hurts throughput but simplifies bounce handling.
received_headerHaraka outboundText used in the outbound Received: header. Set to the literal disabled to omit it.
connect_timeout30Seconds to wait for TCP connect to the remote MX.
local_mx_okfalseAllow outbound delivery to local/private IPs (otherwise blocked to prevent loops).
inet_preferdefaultdefault (prefer IPv6 at equal MX priority), v4, or v6. Delivery still follows MX priority.

TLS configuration is shared with the tls plugin (tls_key.pem, tls_cert.pem, and tls.ini). Outbound-specific overrides go under [outbound] in tls.ini:

[outbound]
ciphers=!DES
minVersion=TLSv1.2

outbound.bounce_message

Template for the bounce message body. See "Bounce Messages" below.

The HMail Object

Most outbound hooks pass an hmail (HMailItem). You rarely need its methods, but these properties are useful:

PropertyDescription
pathFull filesystem path to the queue file.
filenameQueue file's base name.
num_failuresNumber of temp-fail attempts so far.
notesPlain object for plugin state, scoped to this queue item.
todoThe TODOItem describing what to deliver (see below).

The TODO Object

hmail.todo describes the delivery:

PropertyDescription
mail_fromAddress1 — the envelope sender.
rcpt_toAddress1 array — envelope recipients.
domainRecipient domain (a single domain unless always_split is set).
notesThe original transaction.notes. Keys you may set:
notes.outbound_ipIP to bind the outbound socket to. Set via the get_mx hook, not directly.
notes.outbound_heloEHLO domain. Set via the get_mx hook, not directly.
queue_timeWhen the mail was queued (epoch ms).
uuidInherited from the source transaction.uuid.
force_tlsIf true, defer instead of delivering in plaintext.

Outbound Hooks

queue_outbound

Runs before queuing. Returning CONT (or having no hook) queues the mail. OK indicates the plugin queued it itself; the DENY* codes reject the message.

pre_send_trans_email

Parameters: next, connection

Fired by outbound.send_trans_email() before the transaction is serialized to disk. Useful for plugins that synthesize mail programmatically — they can attach final headers or notes here.

send_email

Parameters: next, hmail

Called just before delivery starts. next(DELAY, seconds) defers the attempt.

get_mx

Parameters: next, hmail, domain

Called when delivery begins, with the destination domain. Plugins can override MX lookup; most installs leave Haraka to do DNS. Respond with next(OK, mx) where mx is a HarakaMx object, an array of them, or any HarakaMx-compatible input. Set mx.auth_user / mx.auth_pass to AUTH against the remote, or mx.bind / mx.bind_helo to control source address and EHLO.

deferred

Parameters: next, hmail, { delay, err }

Fired on temporary failure. Return OK to drop the mail silently; return DENYSOFT, seconds to override the retry delay (useful for custom backoff indexed on hmail.num_failures).

bounce

Parameters: next, hmail, error

Fired on permanent failure (5xx). Not called for temp-fails. error may carry:

  • mx — the MX that caused the bounce
  • deferred_rcpt — recipients that eventually bounced after deferrals
  • bounced_rcpt — recipients that bounced outright

Return OK to suppress the DSN to the original sender.

delivered

Parameters: next, hmail, [host, ip, response, delay, port, mode, ok_recips, secured, authenticated]

Fired after a successful delivery. Return codes are ignored; the hook is for logging / accounting.

ElementDescription
hostHostname of the receiving MX.
ipIP we delivered to.
responseRemote SMTP response text (typically includes the remote queue ID).
delaySeconds between queue write and delivery.
portDestination port.
mode'smtp' or 'lmtp'.
ok_recipsAddress1 array of successfully delivered recipients.
securedtrue if STARTTLS succeeded.
authenticatedtrue if outbound AUTH succeeded.

Outbound IP Address

By default the OS routing table chooses the source IP. To pin outbound to a specific IP (per-sender, per-domain, etc.), bind that address to a local interface or alias, then set mx.bind (source IP) and mx.bind_helo (EHLO domain) in your get_mx hook.

Outbound AUTH

Force AUTH for a domain or smart host by returning an MX with auth_user and auth_pass set from the get_mx hook. If the remote end does not advertise AUTH (or no compatible mechanism is found), delivery proceeds without AUTH and a warning is logged.

Bounce Messages

The bounce body comes from config/outbound.bounce_message. Curly-brace template variables are filled in at bounce time:

  • pid — current process id
  • date — bounce timestamp
  • me — contents of config/me
  • from — original sender
  • msgid — original message UUID
  • to — original recipient (or first, for multi-recipient mail)
  • reason — remote server's rejection text

The original message is appended to the bounce.

For HTML bounces, add config/outbound.bounce_message_html (and optionally an inline image in config/outbound.bounce_message_image).

Generating Mail from a Plugin

To create and queue a new message from inside a plugin, use the outbound module:

const outbound = require('./outbound')

const from = 'sender@example.com'
const to = 'user@example.com'

const contents = [
    `From: ${from}`,
    `To: ${to}`,
    'MIME-Version: 1.0',
    'Content-Type: text/plain; charset=us-ascii',
    'Subject: Hello',
    '',
    'Body here.',
    '',
].join('\n')

outbound.send_email(from, to, contents, (code, msg) => {
    switch (code) {
        case OK:
            plugin.loginfo('queued')
            break
        case DENY:
            plugin.logerror(`queue failed: ${msg}`)
            break
    }
})

The callback fires when the mail is queued, not delivered — hook delivered and bounce to observe delivery outcomes.

The callback may be omitted if you don't need to handle queue failure:

outbound.send_email(from, to, contents)

Options accepted by send_email(from, to, contents, next, options):

OptionDescription
dot_stuffed: trueContent is already SMTP dot-stuffed.
notes: { … }Seed the new transaction's notes.
remove_msgid: trueDrop any existing Message-Id: so Haraka generates one. Useful when releasing from quarantine.
remove_date: trueDrop any existing Date: so Haraka generates one.
origin: <object>Object passed to the logger to identify the source plugin / connection / HMailItem.

To send an already-built Transaction directly, use outbound.send_trans_email(transaction, next). This is what send_email() calls internally and fires the pre_send_trans_email hook.

1: Address objects are @haraka/email-address objects.