[−][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 |
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 |
BodyCallback | The type of the |
CloseCallback | The type of the |
ConnectCallback | The type of the |
DataCallback | The type of the |
EohCallback | The type of the |
EomCallback | The type of the |
HeaderCallback | The type of the |
HeloCallback | The type of the |
MailCallback | The type of the |
NegotiateCallback | The type of the |
RcptCallback | The type of the |
Result | A result type specialised for milter errors. |
UnknownCallback | The type of the |