[][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 implementation. 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("could not run milter");
}

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 lifecycle 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 highlights some aspects to be aware of when creating milter applications.

Callback flow

One must be aware of the basic flow of callback calls. The flow is as follows. When negotiation is used, this is the very first step, preceding connect.

Several messages can be processed in a single connection. In that case, the message-oriented stages (mail to eom) will be traversed repeatedly. Message-oriented processing is always bracketed by connection-oriented stages connect and close.

At any point during processing of a message the flow can be derouted to abort, which skips the remaining of the message steps and processing continues at the start of the message loop.

In any case, close will be called at end of processing, so this is the natural place to do cleanup of resources.

Notice that resources can be connection-scoped and message-scoped.

At each stage, a Status is returned that decides whether to continue, reject, etc. Only at the eom stage, message modification operations can be applied, such as adding headers or altering the message body.

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-specific and message-specific data. This is an area that has one manual memory management requirement. Any resources stored in the callback context DataHandle must be reacquired and dropped before the connection closes.

That is, all code paths through a complete callback flow must include a final call to DataHandle::take!

Failure to acquire the data 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, however, 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 remains safe.

As usual, panicking 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 failures 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 is described in the preceding section.

Globals

A milter implementation is fundamentally a singleton within the process. Only one invocation of Milter::run is allowed per process. Therefore, globals are an acceptable and reasonable thing to have.

Nevertheless, the milter library may use multiple threads to handle requests, so any use of global storage should make sure to use a synchronisation mechanism.

Testing

The callback design is not well suited for unit testing.

Instead, it is recommended to do integration-level testing of milter applications. For example, you could use the tool miltertest to drive your integration tests. This tool is part of the opendkim-tools package on Debian and Ubuntu.

Structs

ActionContext

Context supplied to the final 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 managed 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

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 event loop to exit, thereby shutting down the 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.