Haraka Documentation

A fast, highly extensible, and event driven SMTP server

5,608 stars JavaScript JavaScript 1 file ยท ~6,659 tokens #dkim#haraka#javascript#mta#nodejs#smtp#spf
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy Haraka on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name haraka -p 25:25 --restart always haraka/haraka:latest

1. Project Overview & Quickstart (haraka/Haraka)

File: README.md

Haraka โ€” a Node.js Mail Server

Build Cover Qlty

Haraka is a highly scalable Node.js SMTP server with a modular plugin architecture. It handles thousands of concurrent connections and delivers thousands of messages per second. Haraka and its plugins are written in asynchronous JavaScript, optimised for throughput and low latency.

Haraka offers strong spam protection (see Plugins.md) and is widely deployed as a filtering MTA or as a MSA on port 465 (and legacy 587) with the auth and DKIM plugins enabled.

Haraka is not a mail store, an LDA, or an IMAP server. It is designed to work alongside those systems. A scalable outbound delivery engine is built in: mail flagged as relaying (for example, by an auth plugin) is queued for
outbound delivery automatically.

Plugin Architecture

Haraka's defining feature is its plugin system. Every SMTP transaction is a sequence of well-defined hooks โ€” connect, helo, mail, rcpt, data, data_post, queue, and more โ€” and each hook can be extended with a few lines of JavaScript. Plugins are asynchronous by default, so a slow lookup against DNS, Redis, or an HTTP API never blocks the server.

The result is that behaviours which would require a custom MTA elsewhere are typically a small file in Haraka. For example, accepting qmail-style tagged addresses ([email protected]) and rewriting them to [email protected] before forwarding to an Exchange or IMAP backend looks roughly like this:

js
exports.hook_rcpt = (next, connection, params) => {
  const rcpt = params[0]
  const [user] = rcpt.user.split('-')
  rcpt.user = user
  next()
}

A comprehensive registry of community and core plugins โ€” auth, DNSBLs, DKIM, SpamAssassin, rspamd, Redis, ClamAV, queue backends, and many others โ€” lives in Plugins.md. To write your own, see the plugin tutorial.

Documentation

  • Plugins.md โ€” plugin registry and configuration reference
  • docs/ โ€” core documentation (Connection, Transaction, Outbound, โ€ฆ)
  • Tutorial โ€” step-by-step getting started guide
  • CHANGELOG.md โ€” release notes
  • SECURITY.md โ€” security policy and reporting

Getting Help

Installation

Haraka requires Node.js. Install via npm:

sh
npm install Haraka

Create a service directory:

sh
haraka -i /path/to/haraka_test

This creates haraka_test with config/ and plugins/ subdirectories and sets the host name from hostname(1). Edit config/host_list to add the domains for which Haraka should accept mail.

Start Haraka:

sh
haraka -c /path/to/haraka_test

Configuration

Edit config/plugins to select active plugins. By default, mail addressed to domains in config/host_list is accepted and forwarded via the smtp-forward plugin (configured in config/smtp_forward.ini).

Per-plugin documentation is available via:

sh
haraka -h plugins/<name>

See Plugins.md for the full registry.

Running from Source

sh
git clone https://github.com/haraka/Haraka.git
cd Haraka
npm install
node haraka.js

Authorship and Maintenance

Haraka was created by Matt Sergeant (baudehlo), formerly project leader of SpamAssassin and a contributor to Qpsmtpd. The project is currently maintained by
Matt Simerson (msimerson).

Haraka is the work of many hands. See CONTRIBUTORS.md for the full list of people who have contributed code, documentation, and plugins.

License

Haraka is released under the MIT License. See LICENSE for details.


File: docs/deprecated/access.md

access - ACLs

Repackaged as haraka-plugin-access.


File: docs/deprecated/backscatterer.md

backscatterer

This is a very basic pluign that checks the connecting IP against
ips.backscatterer.org when the envelope-from is null or postmaster@
as per the instructions at http://www.backscatterer.org/?target=usage

This plugin is used to reject misdirected bounces and autoresponders
and sender callouts from abusive systems which can happen when a
local domain is spoofed and used as the envelope-from in a spam run.


File: docs/deprecated/connect.rdns_access.md

DEPRECATION NOTICE

See haraka-plugin-access
for upgrade instructions.

connect.rdns_access

This plugin will evaluate the remote IP address and the remote rDNS hostname
against a set of white and black lists. The lists are applied in the following
way:

connect.rdns_access.whitelist (pass)
connect.rdns_access.whitelist_regex (pass)
connect.rdns_access.blacklist (block)
connect.rdns_access.blacklist_regex (block)

Configuration connect.rdns_access.ini

General configuration file for this plugin.

  • connect.rdns_access.general.deny_msg

    Text to send the user on reject (text).

Configuration connect.rdns_access.whitelist

The whitelist is mostly to counter blacklist entries that match more than
what one would want. This file should be used for a specific IP address
or rDNS hostnames, one per line, that should bypass blacklist checks.
NOTE: We heavily suggest tailoring blacklist entries to be as accurate as
possible and never using whitelists. Nevertheless, if you need whitelists,
here they are.

Configuration connect.rdns_access.whitelist_regex

Does the same thing as the whitelist file, but each line is a regex.
Each line is also anchored for you, meaning '^' + regex + '$' is added for
you. If you need to get around this restriction, you may use a '.*' at
either the start or the end of your regex. This should help prevent people
from writing overly permissive rules on accident.

Configuration connect.rdns_access.blacklist

This file should be used for a specific IP address or rDNS hostnames, one
per line, that should fail on connect.

Configuration connect.rdns_access.blacklist_regex

Does the same thing as the blacklist file, but each line is a regex.
Each line is also anchored for you, meaning '^' + regex + '$' is added for
you. If you need to get around this restriction, you may use a '.*' at
either the start or the end of your regex. This should help prevent people
from writing overly permissive rules on accident.


File: docs/deprecated/data.headers.md

data.headers

Deprecated by haraka-plugin-headers


File: docs/deprecated/data.nomsgid.md

data.nomsgid

NOTICE: this plugin is deprecated. Use data.headers instead.

Quite simply enabling this plugin blocks all mails lacking a Message-Id
header. This is an aggressive anti-spam measure, but since most mail systems
will add a Message-Id header, it tends to block a good chunk of abusive mail.


File: docs/deprecated/data.noreceived.md

data.noreceived

NOTICE: this plugin is deprecated. Use data.headers instead.

This plugin very simply blocks any mail arriving at your system that has no
Received headers.

This is an aggressive anti-spam measure, but works because all real mail
relays will add a Received header according to the RFCs. It may false
positive on some bulk mail that uses a custom tool to send, but this appears
to be fairly rare.


File: docs/deprecated/data.rfc5322_header_checks.md

data.rfc5322_header_checks

NOTICE: this plugin is deprecated. Use data.headers instead.

This plugin enforces RFC 5322 Section 3.6 which states that:

All messages MUST have a 'Date' and 'From' header and a message may not contain
more than one 'Date', 'From', 'Sender', 'Reply-To', 'To', 'Cc', 'Bcc',
'Message-Id', 'In-Reply-To', 'References' or 'Subject' header.

Any message that does not meet these requirements will be rejected.


File: docs/deprecated/dkim_sign.md

dkim_sign

This plugin implements the DKIM Core specification.

This plugin only signs outbound messages. It does not validate DKIM signatures.

Getting Started

Generate a DKIM selector and keys for your domain:

sh
cd /path/to/haraka/config/dkim
./dkim_key_gen.sh example.org

Within the config/dkim/${domain} directory will be 4 files:

sh
ls config/dkim/example.org/
dns private public selector

The selector file contains the DNS label where the DKIM public key is published. The private and public files contain the DKIM keys.

The dns file contains a formatted record of the public key suitable for copy/pasting into your domains zone file. It also has suggestions for DKIM, SPF, and DMARC policy records.

The DKIM DNS record will look like this:

text
may2013._domainkey TXT "v=DKIM1;p=[public key stripped of whitespace];"

The values in the address have the following meaning:

text
hash: h=[ sha1 | sha256 ]
test; t=[ s | s:y ]
granularity: g=[ ]
notes: n=[ ]
services: s=[email]
keytypes: [ rsa ]

Key size

The default key size created by dkim_key_gen.sh is 2048. That is considered secure as of mid-2014 but after 2020, you should be using 4096.

What to sign

The DKIM signing key for messages from example.org should be signed with
a DKIM key for example.org. Failing to do so will result in messages not
having an aligned DKIM signature. For DMARC enabled domains, this will
likely result in deliverability problems.

For correct alignment, Haraka signs each message with that domains DKIM key.
For an alternative, see the legacy Single Domain Configuration below.

Configuration

This plugin is configured in dkim_sign.ini.

  • disabled = [ 1 | true | yes ] (OPTIONAL)

    Set this to disable DKIM signing

  • headers_to_sign = list, of; headers (REQUIRED)

    Set this to the list of headers that should be signed, separated by commas, colons or semi-colons. Signing prevents tampering with the specified headers.
    The 'From' header is required by the RFC and will be added if missing.

Single Domain Configuration

To sign all messages with a single DKIM key, you must set the selector and domain in dkim_sign.ini. You must also save your DKIM private key in the file dkim.private.key in the Haraka config directory.

  • selector = name

    Set this to the selector name published in DNS under the
    _domainkey sub-domain of the domain referenced below.

  • domain = name

    Set this to the domain name that will be used to sign messages
    which don't match a per-domain DKIM key. The DNS TXT entry for:

    text
    <selector>._domainkey.<domain>

Test that your DKIM key is published properly with a DNS request like this:

sh
drill TXT $SELECTOR._domainkey.$DOMAIN
dig TXT $SELECTOR._domainkey.$DOMAIN +short

Example DNS query

sh
export SELECTOR=mar2013
export DOMAIN=simerson.net
$ dig TXT $SELECTOR._domainkey.$DOMAIN +short
"v=DKIM1;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoyUzGOTSOmakY8BcxXgi0mN/nFegLBPs7aaGQUtjHfa8yUrt9T2j6GSXgdjLuG3R43WjePQv3RHzc+bwwOkdw0XDOXiztn5mhrlaflbVr5PMSTrv64/cpFQKLtgQx8Vgqp7Dh3jw13rLomRTqJFgMrMHdhIibZEa69gtuAfDqoeXo6QDSGk5JuBAeRHEH27FriHulg5ob" "4F4lmh7fMFVsDGkQEF6jaIVYqvRjDyyQed3R3aTJX3fpb3QrtRqvfn/LAf+3kzW58AjsERpsNCSTD2RquxbnyoR/1wdGKb8cUlD/EXvqtvpVnOzHeSeMEqex3kQI8HOGsEehWZlKd+GqwIDAQAB"

File: docs/deprecated/dkim_verify.md

dkim_verify

This plugin will verify DKIM signatures as defined by RFC 6376 and add
an Authentication-Results header as appropriate.

Configuration

  • allowed_time_skew

    How far can we stretch on time matching, in secs. Useful when clock is skewed.

  • sigerror_log_level

Testing

This plugin also provides a command-line test tool that can be used to
debug DKIM issues or to check results.

text
# dkimverify < message
identity="@gmail.com" domain="gmail.com" result=pass

You can add --debug to the option arguments to see a full trace of the processing.

Notes

This plugin and underlying library does not currently support DKIM body length limits (l=).

2. Official Technical Reference & Guides (haraka/haraka.github.io)

File: README.md

haraka.github.io

Setup

Install dependencies:

bash
npm install

Development

bash
npm run dev

Edge Side Rendering

Can be deployed to Vercel Functions, Netlify Functions, AWS, and most Node-compatible environments.

Look at all the available presets here.

bash
npm run build

Static Generation

Use the generate command to build your application.

The HTML files will be generated in the .output/public directory and ready to be deployed to any static compatible hosting.

bash
npm run generate

Preview build

You might want to preview the result of your build locally, to do so, run the following command:

bash
npm run preview

For a detailed explanation of how things work, check out Docus.


File: content/6.core/1.Address.md


title: Address Object _path: /core/Address

Address Object

The Address object is an interface to reading email addresses passed in at
SMTP time. As such it parses all the formats in RFC-2821 and 2822, and
supports correctly escaping email addresses.

API

  • new Address (user, host)

Create a new address object for user@host

  • new Address (email)

Creates a new address object by parsing the email address. Will throw an
exception if the address cannot be parsed.

  • address.user

Access the local part of the email address

  • address.host

Access the domain part of the email adress

  • address.format()

Provides the email address in the appropriate `` format. And
deals correctly with the null sender and local names.

  • address.toString()

Same as format().

  • address.address

Provides the email address in 'user@host' format.

Advanced Usage

It is possible to mess with the regular expressions used to match addresses
for stricter or less strict matching.

To change the behaviour mess with the following variables:

text
var adr = require('./address');
// Now change one of the following. Note they are RegExp objects NOT strings.
adr.atom_expr;
adr.address_literal_expr;
adr.subdomain_expr;
adr.domain_expr;
adr.qtext_expr;
adr.text_expr;
// Don't forget to recompile:
adr.compile_re();

File: content/6.core/2.Body.md


title: Body Object _path: /core/Body

Body Object

This documentation has moved to haraka/email-message.


File: content/6.core/3.Config.md


title: Config _path: /core/Config

Config

This documentation has moved to haraka-config.


File: content/6.core/4.Connection.md


title: Connection Object _path: /core/Connection

Connection Object

For each connection to Haraka there is one connection object.

API

  • connection.uuid

A unique UUID for this connection.

  • connection.remote - info about the host that is connecting to Haraka.

    • ip - remote IP address
    • host - reverse DNS of the remote hosts IP
    • is_private - true if the remote IP is from a private (loopback, RFC 1918, link local, etc.) IP address.
    • is_local - true if the remote IP is localhost (loopback, link local)
  • connection.local - info about the host that is running Haraka

    • ip - the IP of the Haraka server, as reported by the OS
    • port - the port number handling the connection.
    • host - the rDNS host name of the local IP
  • connection.proxy - proxy properties set when a proxy is used (like haproxy)

    • allowed - if the remote IP has proxy permission
    • ip - when proxied, the proxy servers IP address
    • type - currently null or 'haproxy'
  • connection.hello

    • verb - Either 'EHLO' or 'HELO' whichever the remote end used
    • host - The hostname given with HELO or EHLO
  • connection.notes

An object which persists during the lifetime of the connection. It is used to store connection-specific properties. See also, connection.results and haraka-notes.

  • connection.transaction

The current transaction object, valid after MAIL FROM, and destroyed at queue
time, RSET time, or if MAIL FROM was rejected. See the Transaction Object
documentation file.

  • connection.relaying

A boolean flag to say whether this connection is allowed to relay mails (i.e.
deliver mails outbound). This is normally set by SMTP AUTH, or sometimes via
an IP address check.

  • connection.current_line

For low level use. Contains the current line sent from the remote end,
verbatim as it was sent. Can be useful in certain botnet detection techniques.

  • connection.last_response

Contains the last SMTP response sent to the client.

  • connection.remote_closed

For low level use. This value is set when the remote host drops the connection.

  • connection.results

Store results of processing in a structured format. See docs/Results


File: content/6.core/5.CoreConfig.md


title: Core Configuration Files _path: /core/CoreConfig

See Logging.

The Haraka core reads some configuration files to determine a few actions:

  • smtp.yaml or smtp.json

If either of these files exist then they are loaded first.
This file is designed to use the JSON/YAML file overrides documented in
Config to optionally provide the entire configuration in a single file.

  • plugins

The list of plugins to load

  • smtp.ini

    Keys:

    • listen_host, port - the host and port to listen on (default: ::0 and 25)
    • listen - (default: [::0]:25) Comma separated IP:Port addresses to listen on
    • inactivity_time - how long to let clients idle in seconds (default: 300)
    • nodes - specifies how many processes to fork. The string "cpus" will fork as many children as there are CPUs (default: 1, which enables cluster mode with a single process)
    • user - optionally a user to drop privileges to. Can be a string or UID.
    • group - optionally a group to drop privileges to. Can be a string or GID.
    • ignore_bad_plugins - If a plugin fails to compile by default Haraka will stop at load time.
      If, however, you wish to continue on without that plugin's facilities, then
      set this config option
    • daemonize - enable this to cause Haraka to fork into the background on start-up (default: 0)
    • daemon_log_file - (default: /var/log/haraka.log) where to redirect stdout/stderr when daemonized
    • daemon_pid_file - (default: /var/run/haraka.pid) where to write a PID file to
    • spool_dir - (default: none) directory to create temporary spool files in
    • spool_after - (default: -1) if message exceeds this size in bytes, then spool the message to disk
      specify -1 to disable spooling completely or 0 to force all messages to be spooled to disk.
    • graceful_shutdown - (default: false) enable this to wait for sockets on shutdown instead of closing them quickly
    • force_shutdown_timeout - (default: 30) number of seconds to wait for a graceful shutdown
  • me

    A name to use for this server. Used in received lines and elsewhere. Setup
    by default to be your hostname.

  • connection.ini

    See inline comments in connection.ini for the following settings:

    • haproxy.hosts_ipv4
    • haproxy.hosts_ipv6
    • headers.*
    • max.bytes
    • max.line_length
    • max.data_line_length
    • max.mime_parts
    • message.greeting
    • message.close
    • smtputf8
    • strict_rfc1869
    • uuid.deny_chars
    • uuid.banner_bytes
  • plugin_timeout

    Seconds to allow a plugin to run before the next hook is called automatically
    default: 30

    Note also that each plugin can have a config/.timeout
    file specifying a per-plugin timeout. In this file you can set a timeout of 0 to mean that this plugin's hooks never time out. Use this with care.

    If the plugin is in a sub-directory of plugins, then you must create this file
    in the equivalent path e.g. the queue/smtp_forward would need a timeout file in config/queue/smtp_forward.timeout

  • outbound.ini

  • outbound.bounce_message

    The bounce message if delivery of the message fails. The default is normally fine. Bounce messages contain a number of template replacement values which are best discovered by looking at the source code.


File: content/6.core/6.CustomReturnCodes.md


title: Custom Return Codes _path: /core/CustomReturnCodes

Custom Return Codes

This content has moved to haraka-dsn.


File: content/6.core/7.HAProxy.md


title: HAProxy PROXY protocol extension support navigation.title: HAProxy _path: /core/HAProxy

HAProxy

Haraka natively supports the PROXY protocol 1.

This allows an upstream proxy to pass IP address and port of the client which
Haraka will use instead of the socket IP address (which is of the proxy).
This allows DNSBLs and access control lists to operate on the proxied address.

Support is disabled by default and if HAProxy or other attempts to send a
PROXY command then Haraka will return a DENYSOFTDISCONNECT error.
DENYSOFT is used to prevent configuration errors from rejecting valid mail.

To enable support for PROXY you must create a haproxy_hosts configuration
file which should contain a list of IP addresses of the HAProxy hosts
that should be allowed to send the PROXY command. A range of IP
addresses can be specified by it's CIDR network address.

When a host connects to Haraka that matches an IP address present in the
haproxy_hosts file - a banner is not sent, instead Haraka waits for the
PROXY command to be sent before proceeding. The connection will timeout
with 421 PROXY timed out if the command is not sent within 30 seconds.

NOTE: because Haraka does not send a banner when a listed HAProxy host
connects you must set check-send-proxy to ensure that the service checks
send a PROXY command before they run.

1 http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt

HAProxy supports the PROXY protocol in version 1.5 or later however there
are patches available to add support for 1.4.

Here is an example listener section for haproxy.cfg:

text
listen smtp :25
        mode tcp
        option tcplog
        option smtpchk
        balance roundrobin
        server smtp1 ip.of.haraka.server1:25 check-send-proxy check inter 10s send-proxy
        server smtp2 ip.of.haraka.server2:25 check-send-proxy check inter 10s send-proxy
        server smtp3 ip.of.haraka.server3:25 check-send-proxy check inter 10s send-proxy
        server smtp4 ip.of.haraka.server4:25 check-send-proxy check inter 10s send-proxy
        server smtp5 ip.of.haraka.server5:25 check-send-proxy check inter 10s send-proxy

The important part is send-proxy which causes HAProxy to send the PROXY
extension on connection.

When using option smtpchk you will see CONNRESET errors reported in the Haraka logs as
smtpchk drops the connection before the HELO response is still being written.
You can use the option tcp-check instead to provide a better service check by having
the check wait for the banner, send QUIT and then check the response:

text
option tcp-check
    tcp-check expect rstring ^220\ 
    tcp-check send QUIT\r\n
    tcp-check expect rstring ^221\

File: content/6.core/8.Logging_API.md


title: Logging API _path: /core/Logging_API

Logging API

Logging conventions within Haraka

See also

https://github.com/haraka/Haraka/pull/119

logline will always always be in the form:

text
[level] [connection uuid] [origin] message

where origin is "haraka_core" or the name of the plugin which
triggered the message, and "connection uuid" is the ID of the
connection associated with the message.

when calling a log method on logger, you should provide the
plugin object and the connection object anywhere in the arguments
to the log method.

js
logger.logdebug("i like turtles", plugin, connection);

will yield, for example,

text
[DEBUG] [7F1C820F-DC79-4192-9AA6-5307354B20A6] [dnsbl] i like turtles

if you call the log method on the connection object, you can
forego the connection as argument:

js
connection.logdebug("turtles all the way down", plugin);

and similarly for the log methods on the plugin object:

js
plugin.logdebug("he just really likes turtles", connection);

failing to provide a connection and/or plugin object will leave
the default values in the log (currently "core" and
"no_connection").

this is implemented by testing for argument type in
the logger.js log* method. objects-as-arguments are then sniffed
to try to determine if they're a connection or plugin instance.


File: content/6.core/9.Header.md


title: Header Object _path: /core/Header

Header object

This documentation has moved to haraka/email-message.