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§
- Actions
- Flags representing milter actions.
- Context
- Context supplied to the milter callbacks.
- Context
Api - An accessor to the set of methods that make up the context API.
- Data
Handle - A handle on user data stored in the callback context.
- Milter
- A configurable milter runner.
- Protocol
Opts - Flags representing milter protocol options.
Enums§
- Error
- Various kinds of errors that can occur in a milter application.
- Stage
- The milter protocol stage.
- Status
- The callback response status.
Traits§
- Action
Context - A trait encapsulating the set of action methods available during the
eom
stage. - Macro
Value - A trait for macro lookup.
- SetError
Reply - A trait for setting up a custom SMTP error reply.
Functions§
- set_
debug_ level - Sets the trace debug level of the libmilter library to the given value.
- shutdown
- Instructs the libmilter library to exit its event loop, thereby shutting down any currently running milter.
- version
- Returns the runtime version triple of the libmilter library.
Type Aliases§
- Abort
Callback - The type of the
on_abort
callback function pointer. - Body
Callback - The type of the
on_body
callback function pointer. - Close
Callback - The type of the
on_close
callback function pointer. - Connect
Callback - The type of the
on_connect
callback function pointer. - Data
Callback - 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. - Header
Callback - The type of the
on_header
callback function pointer. - Helo
Callback - The type of the
on_helo
callback function pointer. - Mail
Callback - The type of the
on_mail
callback function pointer. - Negotiate
Callback - The type of the
on_negotiate
callback function pointer. - Rcpt
Callback - The type of the
on_rcpt
callback function pointer. - Result
- A result type specialised for milter errors.
- Unknown
Callback - The type of the
on_unknown
callback function pointer.