Crate milter[][src]

Expand description

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 to libmilter, 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 on the context API structs, as well as flags such as Actions and ProtocolOpts 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 C library. This documentation will speak of ‘libmilter’ or ‘the libmilter library’ where appropriate.

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::*;

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

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

    Ok(Status::Continue)
}

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

    Ok(Status::Continue)
}

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

    Status::Continue
}

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 through a generic DataHandle<T> 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 the ContextApi struct that is part of the context.

The example is complete and ready to run. A call to Milter::run starts the application, passing control to the libmilter 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. Of the connection-scoped and message-scoped stages 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.

Callback resource management

The callback context allows storing connection-local data. Indeed, given that libmilter 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 and error handling

As the libmilter 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.

Finally, two safety hazards concern the context’s generic DataHandle: First, we noted above the possibility of leaking memory in the DataHandle. Second, there is a requirement to select the same generic type argument T when writing out the callback function arguments: see the safety note at Context. For both of these some programmer discipline is necessary.

Globals

According with the design of the libmilter 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 libmilter may use multiple threads to handle callbacks, any use of static items should use an adequate synchronisation mechanism.

Structs

Flags representing milter actions.

Context supplied to the milter callbacks.

An accessor to the set of methods that make up the context API.

A handle on user data stored in the callback context.

A configurable milter runner.

Flags representing milter protocol options.

Enums

Various kinds of errors that can occur in a milter application.

The milter protocol stage.

The callback response status.

Traits

A trait encapsulating the set of action methods available during the eom stage.

A trait for macro lookup.

A trait for setting up a custom SMTP error reply.

Functions

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

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

Returns the runtime version triple of the libmilter library.

Type Definitions

The type of the on_abort callback function pointer.

The type of the on_body callback function pointer.

The type of the on_close callback function pointer.

The type of the on_connect callback function pointer.

The type of the on_data callback function pointer.

The type of the on_eoh callback function pointer.

The type of the on_eom callback function pointer.

The type of the on_header callback function pointer.

The type of the on_helo callback function pointer.

The type of the on_mail callback function pointer.

The type of the on_negotiate callback function pointer.

The type of the on_rcpt callback function pointer.

A result type specialised for milter errors.

The type of the on_unknown callback function pointer.