miltr-server 0.1.0

A miltr server library in pure rust
Documentation

Miltr

This crate is an implementation of the milter protocol used by postfix and sendmail.

A minimum viable use is:

use async_trait::async_trait;
use miltr_common::{actions::{Action, Continue}, commands::Recipient};
use miltr_server::Milter;

struct PrintRcptMilter;

#[async_trait]
impl Milter for PrintRcptMilter {
    type Error = &'static str;

    /// Just print the recipient
    async fn rcpt(&mut self, recipient: Recipient) -> Result<Action, Self::Error> {
        println!("Received recipient: {:?}", recipient);

        Ok(Continue.into())
    }

    /// Abort has to be implemented. It is called at least once per mail
    /// handling an can occur at any time during the milter conversation.
    /// As this milter does not have any state, nothing has to be cleared.
    async fn abort(&mut self) -> Result<Action, Self::Error> {
        Ok(Continue.into())
    }
}

For examples on how to use it, see the ./examples directory.

Safety

This crate uses unsafe_code = "forbid" in it's linting, but is also using cast-possible-truncation = "allow". So use at your own risk.

Semver

This crate follows semver specification with the following exceptions:

  1. Minimum supported rust version:
    A bump to the MSRV is not considered a semver major semver change, only a minor one.
  2. Features starting with _. These are considered 'internal' and 'private'. This is mainly used for fuzz testing. It makes it much easier to fuzz internals directly. No external user should need to enable those features.

Development

Todos

Parse incoming Macros:
In the protocol::commands::optneg::Optneg::parse(…), implement parsing incoming macros. This is currently stubbed out.

Design Decision

This tries to give small 'justifications' about implementation details.

BytesMut and Ownership

It was relatively easy to 'parse' this protocol using BytesMut::(split_to|split_off). This allows all parsed commands to just own their data without any borrowing complexity as well as having parsing logic inside the parse-step (instead of in the access functions/getters on structs).

This incurs some additional allocations but ATM worth the tradeoff in simplicity. This might be optimized in the future.

Length & Math & Overflows

Currently, this library is not strict in handling parameter length.
This means, an implementor can pass data to the milter codec which is to long.

The codec will reject this data in encoding as well as reject data from the network which is to long in decoding.

Rejecting in this case will error out of the milter codec. This behavior is maybe not ideal, but ATM the best I could come up with.

Additionally, this crate suffers from overflow panics. If you pass a parameter (e.g. a Header value) with length usize::MAX on a 32bit system (-~> 4Gi of size), the codec will try to get an item length: name.len() + value.len(). This will overflow and therefore panic in debug mode, wrap in release mode, breaking the connection.

This is a currently accepted tradeoff as emails will probably be much smaller than a usize on a modern system.

This can be implemented in a better way.

Integration tests

To run integration tests, this repository needs a few pieces of software:

  • postfix
  • swaks

The integration tests assume they can call out to these components freely. To avoid having to install them on your system, this repo contains a docker-compose.yml installing a container with all necessary tools.

The tests can then be run:

docker compose run test_milter

External Documentation regarding the Milter protocol

Credits

purepythonmilter

Special credits go to purepythonmilter, a python package containing a complete milter implementation. Without this resource to have a look at "how they did it" this implementation would not have happened.

Anh Vu

Another big thank you goes to Anh Vu (vunpa1711@gmail.com), working student at Retarus who wrote a big part of the integration tests and brought valuable feedback for implementation improvements. Thank you!