Plugins hook into the mail system at 5 main points:
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.
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.
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.
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.
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.
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.