mailfront

Plugin API


Overview

Plugins hook into the mail system at 5 main points:

  1. when the system is started or reset,
  2. when the sender address is received,
  3. when a recipient address is received,
  4. when data is received, and
  5. when the message is completed.

At each of these events, mailfront goes through the list of loaded plugins. For each plugin that has a handler for such an event, mailfront calls that handler. If the handler returns an error, no further handlers are called; otherwise control passes to the next handler. The return code passed back to the protocol is either the error response, if any was encountered, or the first non-error response. If the sender or recipient handlers of all the plugins return no response, the address is considered rejected, and it is not passed on to the back end. This is done to prevent the default configuration from being an open relay. Plugins may modify the sender or recipient address, as well as the message body.

A template plugin is included as a starting point for developing new plugins.

Plugin Structure

A mailfront plugin needs to define exactly one public symbol, "plugin". All other public symbols are ignored. That symbol is to be defined as follows:

struct plugin plugin = {
  .version = PLUGIN_VERSION,
  .flags = 0,
  .commands = commands,
  .init = init,
  .helo = helo,
  .reset = reset,
  .sender = sender,
  .recipient = recipient,
  .data_start = data_start,
  .data_block = data_block,
  .message_end = message_end,
};

All items in this structure except for .version may be omitted if they are not needed. The .version field is a constant set to prevent loading of plugins that were built for an incompatible API. The .flags field controls how certain parts of the plugin are called, and may be zero or more flags values (see below) ored together. The remainder of the fields are hook functions which, if present, are called at the appropriate times in the message handling process.

Note that backend modules have identical structure to plugins described here, except that the single required public symbol is named backend instead of plugin. The backend hook functions are also always the last ones called (with the exception of data_start described below). Protocol modules have an entirely different structure.

Flags

FLAG_NEED_FILE
If set, a temporary file is created and all message data is written to it. The file descriptor for this temporary file is passed to the .data_start and .message_end hooks.

Commands

The commands entry allows for the definition of new SMTP commands in a plugin. To add commands, set it to an array of struct command containing:

struct command
{
  const char* name;
  int (*fn_enabled)(void);
  int (*fn_noparam)(void);
  int (*fn_hasparam)(str* param);
};

The last command in the array must be followed by a termination record, with name set to NULL.

All of the commands provided by plugins are collected together in the order they are found and passed to the protocol module. Commands in plugins override any built-in commands of the same name.

The fn_enabled member is an optional pointer to a function which returns non-zero if the command is available for use. If this function is not present, the command is considered always enabled.

Two command functions are allowed: fn_noparam is for commands that must not have a parameter, and fn_hasparam is for commands that require a parameter. Since there is no regular grammar for SMTP command parameters, the entire text following the command is passed to the function with no modifications except for stripping leading extraneous spaces and the trailing line ending.

Hook Functions

All hook functions return a response pointer or NULL. This structure consists of two elements: an unsigned SMTP response code number and an ASCII message. If the plugin returns a NULL response, processing continues to the next plugin in the chain (ie pass-through). If the plugin returns a response and the response number is greater than or equal to 400 (ie an error), or the number has the RESPONSE_FINAL bit set, then no further hooks in the chain are called. Response numbers less than 400 are treated as acceptance. The first acceptance response is remembered, but subsequent plugins are still called unless the RESPONSE_FINAL bit is set in the number. If the response was an error, the error is passed back through the protocol, otherwise processing continues to the backend. Protocols that do not use the SMTP numbers (such as QMTP) will translate the number into something appropriate. Error numbers between 400 and 499 inclusive are considered "temporary" errors. All others are considered "permanent" failures (ie reject).

All string parameters are passed as type str* and are modifiable. If their value is changed, all subsequent plugins and the backend will see the modified form, as will the protocol module. See the bglibs str documentation module for functions to use in manipulating these objects.

Sender and recipient SMTP parameters are passed as a str* containing a NUL delimited list of KEYWORD=VALUE pairs. If the parameter keyword was not followed by a value in the SMTP conversation, the =VALUE portion will not be present in the string.

Be aware that the sender and recipient hooks may be called before the message data is handled (as with the SMTP protocol) or after (as with the QMQP and QMTP protocol). In either case, the reset hook will always be called at least once before the message is started, and the message_end hook is called after the message has been completely transmitted.

const response* init(void)
This hook is called once after all the plugins have been loaded.
const response* reset(void)
This hook is called when preparing to start a new message, with the intent that all modules will flush any data specific to the message, as well as after error responses to the sender address or data, and after the SMTP HELO command.
const response* helo(str* hostname, str* capabilities)
This hook is called when the SMTP HELO or EHLO commands are issued. As yet nothing actually uses the hostname string. Other protocols will not call this hook. The capabilities variable contains a list of SMTP EHLO response capabilities, each followed by a newline.
const response* sender(str* address, str* params)
This hook is called after a sender email address is transmitted by the client, and is called exactly once per message.
const response* recipient(str* address, str* params)
This hook is called after a sender email address is transmitted by the client, and may be called zero or more times per message.
const response* data_start(int fd)
This hook is called when the sender starts transmitting the message data. Note that the backend is initialized before calling the plugin hooks, in order that plugins may send extra header data to the backend in this hook.
const response* data_block(const char* bytes, unsigned long len)
This hook is called as blocks of data are received from the sender.
const response* message_end(int fd)
This hook is called when the message has been completely transmitted by the sender.

Session Data

The session structure contains all the current session data, including pointers to the protocol module, the backend module, environment variables, temporary message file descriptor, and internal named strings and numbers. Plugins may use these internal named data items to store information for internal use or to pass to other plugins with the following functions. Note that the string and number tables are independent and may contain items with the same names without conflicts. The named strings work like environment variables but are not exposed when subprograms are executed. The numbers work similarly, but the data type is unsigned long instead of a string pointer.

const char* session_protocol(void)
Returns the name of the protocol front end module.
void session_delnum(const char* name)
Delete the named number from the session.
void session_delstr(const char* name)
Delete the named string from the session.
unsigned long session_getnum(const char* name, unsigned long dflt)
Get the named number from the session. If the name is not present, dflt is returned.
int session_hasnum(const char* name, unsigned long* num)
Returns true if the named number is present in the session.
const char* session_getstr(const char* name)
Fetch the named string from the session. If the name is not present, NULL is returned.
void session_setnum(const char* name, unsigned long value)
Set the named number in the session.
void session_setstr(const char* name, const char* value)
Set the named string in the session.

Library Functions

const response* backend_data_block(const char* data, unsigned long len)
This routine writes a block of data directly to the backend. It takes care of handling both writing to the temporary file if it was created or writing directly to the backend module.
const char* getprotoenv(const char* name)
Fetch the environment variable with the given name prefixed by the value of $PROTO. For example, if $PROTO is set to "TCP" (as with tcpserver), then getprotoenv("LOCALIP") will get the environment variable named "TCPLOCALIP".
int scratchfile(void)
Create a new temporary file descriptor opened for reading and writing. The temporary filename is unlinked before returning so that the temporary file will be deleted as soon as it is closed (by the plugin or when mailfront exits).

Hints

Rewriting the message body

Plugins that need to rewrite the message of the body should do so in the message_end hook. Create a new temporary file descriptor with scratchfile() and write the complete new message to it. Then move the new temporary file over to the existing one with the following sequence:

dup2(tmpfd, fd);
close(tmpfd);

Be sure to rewind the original file descriptor with lseek(fd,SEEK_SET,0) before using it, since the file position will normally be at the very end of the data.