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
| Key | Default | Description |
|---|---|---|
disabled | false | Pause outbound delivery while still queuing inbound mail. Reloadable at runtime. |
concurrency_max | 10000 | Maximum concurrent outbound deliveries per worker. Effective total is concurrency_max × nodes. |
enable_tls | true | Use opportunistic STARTTLS on outbound. |
maxTempFailures | 13 | Maximum temp-fail retries before the message bounces. Ignored if temp_fail_intervals is set. |
temp_fail_intervals | derived | Comma-separated <n><unit>[*<count>] pattern. 1m, 5m*2, 1h*3 → [60,300,300,3600,3600,3600] seconds. none bounces on first temp-fail. |
always_split | false | Create one queue file per recipient (instead of one per destination domain). Hurts throughput but simplifies bounce handling. |
received_header | Haraka outbound | Text used in the outbound Received: header. Set to the literal disabled to omit it. |
connect_timeout | 30 | Seconds to wait for TCP connect to the remote MX. |
local_mx_ok | false | Allow outbound delivery to local/private IPs (otherwise blocked to prevent loops). |
inet_prefer | default | default (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:
| Property | Description |
|---|---|
path | Full filesystem path to the queue file. |
filename | Queue file's base name. |
num_failures | Number of temp-fail attempts so far. |
notes | Plain object for plugin state, scoped to this queue item. |
todo | The TODOItem describing what to deliver (see below). |
The TODO Object
hmail.todo describes the delivery:
| Property | Description |
|---|---|
mail_from | Address1 — the envelope sender. |
rcpt_to | Address1 array — envelope recipients. |
domain | Recipient domain (a single domain unless always_split is set). |
notes | The original transaction.notes. Keys you may set: |
notes.outbound_ip | IP to bind the outbound socket to. Set via the get_mx hook, not directly. |
notes.outbound_helo | EHLO domain. Set via the get_mx hook, not directly. |
queue_time | When the mail was queued (epoch ms). |
uuid | Inherited from the source transaction.uuid. |
force_tls | If 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 bouncedeferred_rcpt— recipients that eventually bounced after deferralsbounced_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.
| Element | Description |
|---|---|
host | Hostname of the receiving MX. |
ip | IP we delivered to. |
response | Remote SMTP response text (typically includes the remote queue ID). |
delay | Seconds between queue write and delivery. |
port | Destination port. |
mode | 'smtp' or 'lmtp'. |
ok_recips | Address1 array of successfully delivered recipients. |
secured | true if STARTTLS succeeded. |
authenticated | true 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 iddate— bounce timestampme— contents ofconfig/mefrom— original sendermsgid— original message UUIDto— 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):
| Option | Description |
|---|---|
dot_stuffed: true | Content is already SMTP dot-stuffed. |
notes: { … } | Seed the new transaction's notes. |
remove_msgid: true | Drop any existing Message-Id: so Haraka generates one. Useful when releasing from quarantine. |
remove_date: true | Drop 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.