Technical Overview

Initialization

In addition to its own initialization, libmilter expects a filter to initialize several parameters before calling smfi_main:

  • The callbacks the filter wishes to be called, and the types of message modification it intends to perform (required, see smfi_register).
  • The socket address to be used when communicating with the MTA (required, see smfi_setconn).
  • The number of seconds to wait for MTA connections before timing out (optional, see smfi_settimeout).

If the filter fails to initialize libmilter, or if one or more of the parameters it has passed are invalid, a subsequent call to smfi_main will fail.

Control Flow

The following pseudocode describes the filtering process from the perspective of a set of N MTA's, each corresponding to a connection. Callbacks are shown beside the processing stages in which they are invoked; if no callbacks are defined for a particular stage, that stage may be bypassed. Though it is not shown, processing may be aborted at any time during a message, in which case the xxfi_abort callback is invoked and control returns to MESSAGE.

For each of N connections { For each filter process connection (xxfi_connect) For each filter process helo (xxfi_helo) MESSAGE:For each message in this connection (sequentially) { For each filter process sender (xxfi_envfrom) For each recipient { For each filter process recipient (xxfi_envrcpt) } For each filter { process DATA (xxfi_data) For each header process header (xxfi_header) process end of headers (xxfi_eoh) For each body block process this body block (xxfi_body) process end of message (xxfi_eom) } } For each filter process end of connection (xxfi_close) }

Note: Filters are contacted in order defined in config file.

To write a filter, a vendor supplies callbacks to process relevant parts of a message transaction. The library then controls all sequencing, threading, and protocol exchange with the MTA. Figure 3 outlines control flow for a filter process, showing where different callbacks are invoked.

Figure 3: Milter callbacks related to an SMTP transaction.
SMTP Commands Milter Callbacks
(open SMTP connection) xxfi_connect
HELO ... xxfi_helo
MAIL From: ... xxfi_envfrom
RCPT To: ... xxfi_envrcpt
[more RCPTs] [xxfi_envrcpt]
DATA xxfi_data
Header: ... xxfi_header
[more headers] [xxfi_header]
xxfi_eoh
body... xxfi_body
[more body...] [xxfi_body]
. xxfi_eom
QUIT xxfi_close
(close SMTP connection)

Note that although only a single message is shown above, multiple messages may be sent in a single connection. Note also that a message or connection may be aborted by either the remote host or the MTA at any point during the SMTP transaction. f this occurs during a message (between the MAIL command and the final "."), the filter's xxfi_abort routine will be called. xxfi_close is called any time the connection closes.

Multithreading

A single filter process may handle any number of connections simultaneously. All filtering callbacks must therefore be reentrant, and use some appropriate external synchronization methods to access global data. Furthermore, since there is not a one-to-one correspondence between threads and connections (N connections mapped onto M threads, M <= N), connection-specific data must be accessed through the handles provided by the Milter library. The programmer cannot rely on library-supplied thread-specific data blocks (e.g., pthread_getspecific(3)) to store connection-specific data. See the API documentation for smfi_setpriv and smfi_getpriv for details.

Resource Management

Since filters are likely to be long-lived, and to handle many connections, proper deallocation of per-connection resources is important. The lifetime of a connection is bracketed by calls to the callbacks xxfi_connect and xxfi_close. Therefore connection-specific resources (accessed via smfi_getpriv and smfi_setpriv) may be allocated in xxfi_connect, and should be freed in xxfi_close. For further information see the discussion of message- versus connection-oriented routines. In particular, note that there is only one connection-specific data pointer per connection.

Each message is bracketed by calls to xxfi_envfrom and xxfi_eom (or xxfi_abort), implying that message-specific resources can be allocated and reclaimed in these routines. Since the messages in a connection are processed sequentially by each filter, there will be only one active message associated with a given connection and filter (and connection-private data block). These resources must still be accessed through smfi_getpriv and smfi_setpriv, and must be reclaimed in xxfi_abort.

Signal Handling

libmilter takes care of signal handling, the filters are not influenced directly by signals. There are basically two types of signal handlers:

  1. Stop: no new connections from the MTA will be accepted, but existing connections are allowed to continue.
  2. Abort: all filters will be stopped as soon as the next communication with the MTA happens.

Filters are not terminated asynchronously (except by signals that can't be caught). In the case of Abort the xxfi_abort callback is invoked.