Permalink
Fetching contributors…
Cannot retrieve contributors at this time
344 lines (238 sloc) 13.9 KB

Plugins

All aspects of receiving an email in Haraka are controlled via plugins. No mail can 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.

Get a list of built-in plugins by running:

haraka -l -c /path/to/config

Display the help text for a plugin by running:

haraka -h <name> -c /path/to/config

Omit the -c /path/to/config to see only the plugins supplied with Haraka (not your local plugins in your config directory).

Writing Haraka Plugins

Anatomy of a Plugin

Plugins in Haraka are Javascript files or modules in the plugins/ directory. Alternatively they can be npm-style node_modules in either the core node_modules folder, or the folder you gave to haraka -i <folder>. See "Plugins as Modules" below for more information on this.

To enable a plugin, add its name to config/plugins.

Register a Hook

There are two ways for plugins to register hooks. Both examples register a function on the rcpt hook:

  1. 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(); };

  2. The hook_[$name] syntax:

    exports.hook_rcpt = function (next, connection, params) { // do processing next(); };

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:

  1. code is one of the listed return codes.
  2. 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() and next(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 by rcpt plugins to accept a recipient and queue plugins when the queue was successful.

    After a plugin calls next(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 the unrecognized_command hook. It instructs Haraka to run a different plugin hook. The msg 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

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.

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 successfull 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

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 passes hmail 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 Javascript objects - use them as simple key/value store e.g.

connection.transaction.notes.test = 'testing';

Plugins as Modules

Plugins loaded as modules behave slightly differently to plugins loaded from plugins/ as plain javascript files.

Plain javascript plugins have a customized version of require() which allows you to load core Haraka modules via specifying require('./name') (note the ./ prefix). Although the core modules aren't in the same folder, we intercept 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 this special require(). In order to load a core Haraka module you will have to use this.core_require('name'). Note that this should be preferred anyway for plain javascript plugins anyway, as the ./ hack is likely to go away in the future.

Plugins loaded as modules also are not compiled inside the Haraka plugin sandbox, which provides some security benefits, but also 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.

See also, Results

Further Reading

Read about the Connection object.

Outbound hooks are also documented.