indymilter-test 0.2.0

Milter testing library
Documentation
use byte_strings::c_str;
use indymilter::{Callbacks, ContextActions, SetErrorReply};
use std::{net::Ipv4Addr, time::Duration};
use tokio::{net::TcpListener, sync::oneshot};

use indymilter_test::*;

const LOCALHOST: (Ipv4Addr, u16) = (Ipv4Addr::LOCALHOST, 0);

#[tokio::test]
async fn basic() {
    let _ = tracing_subscriber::fmt::try_init();

    let listener = TcpListener::bind(LOCALHOST).await.unwrap();
    let callbacks = make_milter_callbacks();
    let config = Default::default();
    let (shutdown_milter, shutdown) = oneshot::channel();

    let milter_addr = listener.local_addr().unwrap();

    let milter = tokio::spawn(indymilter::run(listener, callbacks, config, shutdown));

    let mut conn = TestConnection::configure()
        .read_timeout(Duration::from_secs(10))
        .write_timeout(Duration::from_secs(10))
        .available_actions(Actions::ADD_HEADER)
        .open_tcp(milter_addr)
        .await
        .unwrap();

    assert_eq!(conn.negotiated_actions(), Actions::ADD_HEADER);

    let status = conn.connect("client.example.org", [123, 123, 123, 123])
        .await
        .unwrap();
    assert_eq!(status, Status::Continue);

    let status = conn.helo("mail.example.org").await.unwrap();
    assert_eq!(status, Status::Continue);

    conn.macros(MacroStage::Mail, [("{auth_authen}", "from@example.org")])
        .await
        .unwrap();

    let status = conn.mail(["<from@example.org>"]).await.unwrap();
    assert_eq!(status, Status::Continue);

    let (actions, status) = conn.eom().await.unwrap();

    assert_eq!(
        status,
        Status::Reject {
            message: Some(c_str!("550 No.").into()),
        }
    );

    assert!(actions.has_add_header("X-Name", "X-Value"));

    conn.close().await.unwrap();

    shutdown_milter.send(()).unwrap();

    milter.await.unwrap().unwrap();
}

fn make_milter_callbacks() -> Callbacks<()> {
    use indymilter::Status;

    Callbacks::new()
        .on_negotiate(|_cx, _, _| Box::pin(async { Status::AllOpts }))
        .on_connect(|_cx, _, _| Box::pin(async { Status::Continue }))
        .on_helo(|_cx, _| Box::pin(async { Status::Continue }))
        .on_mail(|_cx, _| Box::pin(async { Status::Continue }))
        .on_eom(|cx| {
            Box::pin(async move {
                cx.actions.add_header("X-Name", "X-Value").await.unwrap();
                cx.reply.set_error_reply("550", None, ["No."]).unwrap();
                Status::Reject
            })
        })
}