Plugins
Most aspects of receiving an email in Haraka are controlled by plugins. Mail cannot even be received unless at least a 'rcpt' and 'queue' plugin are enabled.
Recipient (rcpt) plugins determine if a particular recipient is allowed to be relayed or received for. A queue plugin queues the message somewhere - normally to disk or to an another SMTP server.
Plugin Lists
Get a list of installed plugins by running haraka -l
. To include locally installed plugins, add the -c /path/to/config
option.
We also have a registry of known plugins.
Display the help text for a plugin by running:
haraka -h <name> -c /path/to/config`
Writing Haraka Plugins
Anatomy of a Plugin
Plugins in Haraka are JS files in the plugins
directory (legacy) and npm
modules in the node_modules directory. See "Plugins as Modules" below.
Plugins can be installed in the Haraka global directory (default:
/$os/$specific/lib/node_modules/Haraka) or in the Haraka install directory
(whatever you chose when you typed haraka -i
. Example: haraka -i /etc/haraka
To enable a plugin, add its name to config/plugins
. For npm packaged plugins, the name does not include the haraka-plugin
prefix.
Register
Register is the only plugin function that is syncronous and receives no arguments. Its primary purpose is enabling your plugin to register SMTP hooks. It is also used for syncronous initialization tasks such as loading a config file. For heavier initialization tasks such as establishing database connections, look to init_master
and init_child
instead.
Register a Hook
There are two ways for plugins to register hooks. Both examples register a function on the rcpt hook:
- The
register_hook
function in register():
exports.register = function() { this.register_hook('rcpt', 'my_rcpt_validate'); };
exports.my_rcpt_validate = function (next, connection, params) { // do processing next(); }; - The hook_$name syntax:
exports.hook_rcpt = function (next, connection, params) { // do processing next(); };
The register_hook function within register()
offers a few advantages:
1. register a hook multiple times (see below)
2. a unique function name in stack traces
3. [a better function name](https://google.com/search?q=programming%20good%20function%20names)
4. hooks can be registered conditionally (ie, based on a config setting)
Register a Hook Multiple Times
To register the same hook more than once, call register_hook()
multiple times with the same hook name:
exports.register = function() {
this.register_hook('queue', 'try_queue_my_way');
this.register_hook('queue', 'try_queue_highway');
};
When try_queue_my_way()
calls next()
, the next function registered on hook queue will be called, in this case, try_queue_highway()
.
Determine hook name
When a single function runs on multiple hooks, the function can check the hook property of the connection or hmail argument to determine which hook it is running on:
exports.register = function() {
this.register_hook('rcpt', 'my_rcpt');
this.register_hook('rcpt_ok', 'my_rcpt');
};
exports.my_rcpt = function (next, connection, params) {
var hook_name = connection.hook; // rcpt or rcpt_ok
// email address is in params[0]
// do processing
}
Next()
After registering a hook, functions are called with that hooks arguments (see Available Hooks below. The first argument is a callback function, conventionally named next
. When the function is completed, it calls next()
and the connection continues. Failing to call next()
will result in the connection hanging until that plugin's timer expires.
next([code, msg])
accepts two optional parameters:
code
is one of the listed return codes.msg
is a string to send to the client in case of a failure. Use an array to send a multi-line message.msg
should NOT contain the code number - that is handled by Haraka.
Next() Return Codes
These constants are in your plugin when it is loaded, you do not need to define them:
- CONT
Continue and let other plugins handle this particular hook. This is the default. These are identical:next()
andnext(CONT)
; - DENY - Reject with a 5xx error.
- DENYSOFT - Reject with a 4xx error.
- DENYDISCONNECT - Reject with a 5xx error and immediately disconnect.
- DISCONNECT - Immediately disconnect
- OK
Required byrcpt
plugins to accept a recipient andqueue
plugins when the queue was successful.
After a plugin callsnext(OK)
, no further plugins on that hook will run.
Exceptions to next(OK):- connect_init and disconnect hooks are always called.
- On the deny hook,
next(OK)
overrides the default CONT.
- HOOK_NEXT
HOOK_NEXT is only available on theunrecognized_command
hook. It instructs Haraka to run a different plugin hook. Themsg
argument must be set to the name of the hook to be run. Ex:next(HOOK_NEXT, 'rcpt_ok');
Available Hooks
These are the hook and their parameters (next excluded):
- init_master - called when the main (master) process is started
- init_child - in cluster, called when a child process is started
- init_http - called when Haraka is started.
- init_wss - called after init_http
- connect_init - used to init data structures, called for every connection
- lookup_rdns - called to look up the rDNS - return the rDNS via
next(OK, rdns)
- connect - called after we got rDNS
- capabilities - called to get the ESMTP capabilities (such as STARTTLS)
- unrecognized_command - called when the remote end sends a command we don't recognise
- disconnect - called upon disconnect
- helo (hostname)
- ehlo (hostname)
- quit
- vrfy
- noop
- rset
- mail (from, esmtp_params)
- rcpt (to, esmtp_params)
- rcpt_ok (to)
- data - called at the DATA command
- data_post - called at the end-of-data marker
- max_data_exceeded - called when the message exceeds connection.max_bytes
- queue - called to queue the mail
- queue_outbound - called to queue the mail when connection.relaying is set
- queue_ok - called when a mail has been queued successfully
- reset_transaction - called before the transaction is reset (via RSET, or MAIL)
- deny - called when a plugin returns DENY, DENYSOFT or DENYDISCONNECT
- get_mx (hmail, domain) - called by outbound to resolve the MX record
- deferred (hmail, params) - called when an outbound message is deferred
- bounce (hmail, err) - called when an outbound message bounces
- delivered (hmail, host, ip, response, delay, port, mode, ok_recips, secured, authenticated) - called when outbound mail is delivered
- send_email (hmail) - called when outbound is about to be sent
- pre_send_trans_email (fake_connection) - called just before an email is queued to disk with a faked connection object
rcpt
The rcpt hook is slightly special.
When connection.relaying == false (the default, to avoid being an open relay), a rcpt plugin MUST return next(OK)
or the sender will receive the error message "I cannot deliver for that user". The default rcpt plugin is rcpt_to.in_host_list, which lists the domains for which to accept email.
After a rcpt plugin calls next(OK)
, the rcpt_ok hook is run.
If a plugin prior to the rcpt hook sets connection.relaying = true, then it is not necessary for a rcpt plugin to call next(OK)
.
connect_init
The connect_init
hook is unique in that all return codes are ignored. This is so that plugins that need to do initialization for every connection can be assured they will run. Return values are ignored.
hook_init_http (next, Server)
If http listeners are are enabled in http.ini and the express module loaded, the express library will be located at Server.http.express. More importantly, the express app / instance will be located at Server.http.app. Plugins can register routes on the app just as they would with any Express.js app.
hook_init_wss (next, Server)
If express loaded, an attempt is made to load ws, the websocket server. If it succeeds, the wss server will be located at Server.http.wss. Because of how websockets work, only one websocket plugin will work at a time. One plugin using wss is watch.
pre_send_trans_email (next, fake_connection)
The fake
connection here is a holder for a new transaction object. It only has the log methods and a transaction
property
so don't expect it to behave like a a real connection object. This hook is designed so you can add headers and modify mails
sent via outbound.send_email()
, see the dkim_sign plugin for an example.
Hook Order
The ordering of hooks is determined by the SMTP protocol. Knowledge of RFC 5321 is beneficial.
Typical Inbound Connection
- hook_connect_init
- hook_lookup_rdns
- hook_connect
- hook_helo OR hook_ehlo (EHLO is sent when ESMTP is desired which allows extensions
such as STARTTLS, AUTH, SIZE etc.)
- hook_helo
- hook_ehlo
- hook_capabilities
- hook_unrecognized_command is run for each ESMTP extension the client requests e.g. STARTTLS, AUTH etc.)
- hook_mail
- hook_rcpt (once per-recipient)
- hook_rcpt_ok (for every recipient that hook_rcpt returned
next(OK)
for) - hook_data
- attachment hooks
- hook_data_post
- hook_queue OR hook_queue_outbound
- hook_queue_ok (called if hook_queue or hook_queue_outbound returns
next(OK)
)
- hook_quit OR hook_rset OR hook_helo OR hook_ehlo (after a message has been sent or rejected, the client can disconnect or start a new transaction with RSET, EHLO or HELO)
- hook_reset_transaction
- hook_disconnect
Typical Outbound mail
By 'outbound' we mean messages using Haraka's built-in queue and delivery
mechanism. The Outbound queue is used when connection.relaying = true
is set during the transaction and hook_queue_outbound
is called to queue the message.
The Outbound hook ordering mirrors the Inbound hook order above until after hook_queue_outbound
, which is followed by:
- hook_send_email
- hook_get_mx
- at least one of:
- hook_delivered (once per delivery domain with at least one successful recipient)
- hook_deferred (once per delivery domain where at least one recipient or connection was deferred)
- hook_bounce (once per delivery domain where the recipient(s) or message was rejected by the destination)
Plugin Run Order
Plugins are run on each hook in the order that they are specified in config/plugins
. When a plugin returns anything other than next()
on a hook, all subsequent plugins due to run on that hook are skipped (exceptions: connect_init, disconnect).
This is important as some plugins might rely on results
or notes
that have been set by plugins that need to run before them. This should be noted in the plugins documentation. Make sure to read it.
If you are writing a complex plugin, you may have to split it into multiple plugins to run in a specific order e.g. you want hook_deny to run last after all other plugins and hook_lookup_rdns to run first, then you can explicitly register your hooks and provide a priority
value which is an integer between -100 (highest priority) to 100 (lowest priority) which defaults to 0 (zero) if not supplied. You can apply a priority to your hook in the following way:
exports.register = function() {
var plugin = this;
plugin.register_hook('connect', 'hook_connect', -100);
}
This would ensure that your hook_connect function will run before any other
plugins registered on the connect
hook, regardless of the order it was
specified in config/plugins
.
Check the order that the plugins will run on each hook by running:
haraka -o -c /path/to/config
Skipping Plugins
Plugins can be skipped at runtime by pushing the name of the plugin into the skip_plugins
array in transaction.notes
. This array is reset for every transaction and once a plugin is added to the list, it will not run any hooks in that plugin for the remainder of the transaction. For example, one could create a whitelist plugin that skipped spamassassin
if the sender was in a whitelist.
Logging
Plugins inherit all the logging methods of logger.js
, which are:
- logprotocol
- logdebug
- loginfo
- lognotice
- logwarn
- logerror
- logcrit
- logalert
- logemerg
If plugins throw an exception when in a hook, the exception will be caught and generate a logcrit level error. However, exceptions will not be caught as gracefully when plugins are running async code. Use error codes for that, log the error, and run your next() function appropriately.
Sharing State
There are several cases where you might need to share information between
plugins. This is done using notes
- there are three types available:
- server.notes
Available in all plugins. This is created at PID start-up and is shared amongst all plugins on the same PID and listener. Typical uses for notes at this level would be to share database connections between multiple plugins or connection pools etc. - connection.notes
Available on any hook that passes 'connection' as a function parameter. This is shared amongst all plugins for a single connection and is destroyed after the client disconnects. Typical uses for notes at this level would be to store information about the connected client e.g. rDNS names, HELO/EHLO, white/black list status etc. - connection.transaction.notes
Available on any hook that passes 'connection' as a function parameter between hook_mail and hook_data_post. This is shared amongst all plugins for this transaction (e.g. MAIL FROM through until a message is received or the connection is reset). Typical uses for notes at this level would be to store information on things like greylisting which uses client, sender and recipient information etc. - hmail.todo.notes
Available on any outbound hook that passeshmail
as a function parameter. This is the same object as 'connection.transaction.notes', so anything you store in the transaction notes is automatically available in the outbound functions here.
All of these notes are JS objects - use them as simple key/value store e.g.
connection.transaction.notes.test = 'testing';
Plugins as Modules
Plugins as NPM modules are named with the haraka-plugin
prefix. Therefore, a
plugin that frobnobricates might be called haraka-plugin-frobnobricate
and
published to NPM with that name. The prefix is not required in the
config/plugins
file.
Plugins loaded as NPM modules behave slightly different than plugins loaded as plain JS files.
Plain JS plugins have a custom require()
which allows loading core Haraka
modules via specifying require('./name')
(note the ./
prefix). Although
the core modules aren't in the same folder, the custom require
intercepts
this and look for core modules. Note that if there is a module in your plugins
folder of the same name that will not take preference, so avoid using names
similar to core modules.
Plugins loaded as modules do not have the special require()
. To load
a core Haraka module you must use this.haraka_require('name')
.
This should also be preferred for plain JS plugins, as the
./
hack is likely to be removed in the future.
Plugins loaded as modules are not compiled in the Haraka plugin sandbox,
which blocks access to certain globals and provides a global server
object.
To access the server
object, use connection.server
instead.
Module plugins support default config in their local config
directory. See the
"Default Config and Overrides" section in Config.
Shutdown
On graceful reload, Haraka will call a plugin's shutdown
method.
This is so you can clear any timers or intervals, or shut down any connections to remote servers. See Issue 2024.
e.g.
exports.shutdown = function () {
clearInterval(this._interval);
}
If you don't implement this in your plugin and have a connection open or a timer running then Haraka will take 30 seconds to shut down and have to forcibly kill your process.
Note: This only applies when running with a nodes=...
value in smtp.ini.
See also, Results
Further Reading
Read about the Connection object.
Outbound hooks are also documented.