[][src]Crate milter

A library for writing milters: mail filtering applications that can be integrated with MTAs (mail servers) over the sendmail milter protocol.

This crate contains the Rust bindings for libmilter, that is, the sendmail mail filter API. As such, it does not try to hide the nature of that venerable C library, but exposes its capabilities faithfully with all its quirks. If you have used libmilter before, the functionality exposed through the Context and ActionContext structs, as well as flags such as Actions will be immediately familiar, though some of the names have been adapted.

Once it has started up, a milter application is driven by the underlying libmilter C library. This documentation will speak of the ‘milter library’ in those cases.

Usage

To give an idea of how to use this crate, let’s create a milter that counts the envelope recipients of a message, and adds a header recording the count.

This simple example demonstrates all important aspects of a milter application: handling of SMTP events with callbacks (each envelope recipient), storing data in the callback context (the recipient count), and finally performing some message modification operation (adding a header).

use milter::*;

#[on_rcpt(rcpt_callback)]
fn handle_rcpt(context: Context<u32>, _: Vec<&str>) -> milter::Result<Status> {
    match context.data.borrow_mut()? {
        Some(mut count) => *count += 1,
        None => {
            context.data.replace(1)?;
        }
    }

    Ok(Status::Continue)
}

#[on_eom(eom_callback)]
fn handle_eom(context: ActionContext<u32>) -> milter::Result<Status> {
    if let Some(count) = context.data.take()? {
        context.add_header("X-Rcpt-Count", &count.to_string())?;
    }

    Ok(Status::Continue)
}

#[on_abort(abort_callback)]
fn handle_abort(context: Context<u32>) -> Status {
    let _ = context.data.take();

    Status::Continue
}

fn main() {
    Milter::new("inet:3000@localhost")
        .name("RcptMilter")
        .on_rcpt(rcpt_callback)
        .on_eom(eom_callback)
        .on_abort(abort_callback)
        .actions(Actions::ADD_HEADER)
        .run()
        .expect("milter execution failed");
}

A milter’s behaviour is implemented as callback functions that get called as certain events happen during an SMTP conversation. Callback functions are marked up with attribute macros. For example, on_rcpt, called for each RCPT TO command or envelope recipient.

All callback functions return a response Status that determines how to proceed after completing the callback. The callbacks in the example all return Continue, meaning ‘proceed to the next stage’.

The callback functions are then configured on a Milter instance in main. Milter serves as the entry point to configuring and running a milter application.

The example also shows how to store data in the callback context. Context storage is accessible via a DataHandle exposed on the Context struct. A thing to keep in mind is that management of the data’s life cycle is not entirely automatic; in order to avoid leaking memory, care must be taken to reacquire (and drop) the data before the connection closes. In our example this is done in handle_abort, implemented just for this purpose.

Finally, the on_eom end-of-message callback is the place where actions may be applied to a message. These actions – such as adding a header – can be found as methods of ActionContext.

The example is complete and ready to run. A call to Milter::run starts the application, passing control to the milter library. A running milter can be stopped by sending a termination signal, for example by pressing Control-C.

The remainder of this module documentation discusses some topics to be aware of when creating milter applications.

Callback flow

For milter writing one must have an understanding of the ‘flow’ of callback calls. This flow mirrors the succession of events during an SMTP conversation.

The callback flow is as follows (when negotiation is used, it is the very first step, preceding connect):

Several messages may be processed in a single connection. When that is the case, the message-scoped stages mail to eom will be traversed repeatedly. Among the message-scoped processing steps the ones indicated may be executed repeatedly. The message-scoped stages are always bracketed by the connection-scoped stages connect and close.

At any point during processing of a message the flow may be diverted to abort, in which case the remaining message stages are skipped and processing continues at the beginning of the message loop. In any case close will be called at the very end.

For each stage, a response status returned from the callback determines what to do with the entity being processed: whether to continue, accept, or reject it. Only at the eom (end-of-message) stage may message modification operations such as adding headers or altering the message body be applied.

Further detail on this and on the high-level design of the milter library can be found in its documentation.

Callback resource management

The callback context allows storing connection-local data. Indeed, given that the milter library may employ multiple threads of execution for handling requests, all data shared across callback functions must be accessed using that DataHandle.

Context data need to be allocated and released at an appropriate place in the callback flow. From the previous section it follows that resources may logically be connection-scoped or message-scoped. For cleaning up message-scoped resources, eom and abort are the natural stages to do so, whereas for connection-scoped resources it is the close stage.

Note that callback resource management is not automatic. Take care to reacquire and drop any resources stored in the callback context before the connection closes. As a rule of thumb, all paths through the callback flow must include a final call to DataHandle::take. Failure to drop the data in time causes that memory to leak.

Safety

As the milter library is written in C, your Rust callback code is ultimately always invoked by a foreign, C caller. Thanks to the attribute macro-generated conversion layer, your code is safe even in the presence of panics: In Rust, panicking across an FFI boundary is undefined behaviour; the macro-generated layer catches unwinding panics, and so panicking in user code remains safe.

As usual, panic is treated as a fatal error. A panic triggered in a callback results in milter shutdown.

A less extreme failure mode can be chosen by wrapping the callback return type in milter::Result, for example milter::Result<Status> instead of Status. Then, the ? operator can be used to propagate unanticipated errors out of the callback. An Err result corresponds to a Tempfail response and the milter does not shut down.

A further safety concern is the memory leak hazard present in the context’s DataHandle. This was discussed above.

Globals

According with the design of the milter library, a milter application is a singleton (one and only one instance). Only a single invocation of Milter::run is allowed to be active at a time per process. Therefore, global variables are an acceptable and reasonable thing to have.

Nevertheless, as the milter library may use multiple threads to handle callbacks, any use of static items should use an adequate synchronisation mechanism.

Testing

The design of this crate is not well suited for unit testing.

Instead, it is recommended to set up integration-level testing for milter applications. For example, the test suite of the milter crate itself uses the miltertest tool for integration testing. This tool is part of the opendkim-tools package on Debian and Ubuntu.

Structs

ActionContext

Context supplied to the eom milter callback, where message-modifying actions can be taken.

Actions

Flags representing milter actions.

Context

Context supplied to the milter callbacks.

DataHandle

A handle on user data stored in the callback context.

Error

Errors of different kinds specialised for milters, with source attached if available.

Milter

A configurable milter runner.

ProtocolOpts

Flags representing milter protocol options.

Enums

ErrorKind

Various kinds of errors that can occur in a milter.

Stage

The milter protocol stage.

Status

Callback response status.

Functions

set_debug_level

Sets the trace debug level of the milter library to the given value.

shutdown

Instructs the milter library to exit its event loop, thereby shutting down any currently running milter.

version

Returns the runtime version of the milter library.

Type Definitions

AbortCallback

The type of the on_abort callback function pointer.

BodyCallback

The type of the on_body callback function pointer.

CloseCallback

The type of the on_close callback function pointer.

ConnectCallback

The type of the on_connect callback function pointer.

DataCallback

The type of the on_data callback function pointer.

EohCallback

The type of the on_eoh callback function pointer.

EomCallback

The type of the on_eom callback function pointer.

HeaderCallback

The type of the on_header callback function pointer.

HeloCallback

The type of the on_helo callback function pointer.

MailCallback

The type of the on_mail callback function pointer.

NegotiateCallback

The type of the on_negotiate callback function pointer.

RcptCallback

The type of the on_rcpt callback function pointer.

Result

A result type specialised for milter errors.

UnknownCallback

The type of the on_unknown callback function pointer.