example/
example.rs

1//! The example milter included in the libmilter distribution. See:
2//! https://salsa.debian.org/debian/sendmail/blob/master/libmilter/example.c
3
4use milter::{
5    on_abort, on_body, on_close, on_data, on_eoh, on_eom, on_header, on_mail, on_negotiate,
6    on_unknown, Actions, Context, Error, Milter, ProtocolOpts, Status,
7};
8use once_cell::sync::Lazy;
9use std::{
10    env,
11    fs::{self, File, OpenOptions},
12    io::Write,
13    path::PathBuf,
14    process,
15    sync::Mutex,
16    time::SystemTime,
17};
18
19struct Message {
20    path: PathBuf,
21    file: File,
22}
23
24static MTA_CAPS: Lazy<Mutex<ProtocolOpts>> = Lazy::new(|| Mutex::new(ProtocolOpts::empty()));
25
26fn set_mta_caps(protocol_opts: ProtocolOpts) {
27    *MTA_CAPS.lock().unwrap() = protocol_opts;
28}
29
30fn mta_caps() -> ProtocolOpts {
31    *MTA_CAPS.lock().unwrap()
32}
33
34#[on_negotiate(negotiate_callback)]
35fn handle_negotiate(
36    _: Context<Message>,
37    _: Actions,
38    protocol_opts: ProtocolOpts,
39) -> (Status, Actions, ProtocolOpts) {
40    set_mta_caps(protocol_opts);
41
42    let actions = Actions::ADD_HEADER;
43
44    let mut protocol_opts =
45        ProtocolOpts::NO_CONNECT | ProtocolOpts::NO_HELO | ProtocolOpts::NO_RCPT;
46    if mta_caps().contains(ProtocolOpts::NOREPLY_HEADER) {
47        protocol_opts |= ProtocolOpts::NOREPLY_HEADER;
48    }
49
50    (Status::Continue, actions, protocol_opts)
51}
52
53#[on_mail(mail_callback)]
54fn handle_mail(mut ctx: Context<Message>, _: Vec<&str>) -> milter::Result<Status> {
55    let epoch_secs = SystemTime::now()
56        .duration_since(SystemTime::UNIX_EPOCH)
57        .expect("system time out of range")
58        .as_secs();
59    let path = PathBuf::from(format!("/tmp/msg.{}", epoch_secs));
60
61    let file = match OpenOptions::new()
62        .write(true)
63        .truncate(true)
64        .create(true)
65        .open(&path)
66    {
67        Ok(file) => file,
68        Err(_) => return Ok(Status::Tempfail),
69    };
70
71    ctx.data.replace(Message { path, file })?;
72
73    Ok(Status::Continue)
74}
75
76#[on_data(data_callback)]
77fn handle_data(_: Context<Message>) -> Status {
78    Status::Continue
79}
80
81#[on_header(header_callback)]
82fn handle_header(mut ctx: Context<Message>, name: &str, value: &str) -> milter::Result<Status> {
83    let msg = ctx.data.borrow_mut().unwrap();
84
85    write!(&mut msg.file, "{}: {}\r\n", name, value).map_err(|e| Error::Custom(e.into()))?;
86
87    Ok(if mta_caps().contains(ProtocolOpts::NOREPLY_HEADER) {
88        Status::Noreply
89    } else {
90        Status::Continue
91    })
92}
93
94#[on_eoh(eoh_callback)]
95fn handle_eoh(mut ctx: Context<Message>) -> milter::Result<Status> {
96    let msg = ctx.data.borrow_mut().unwrap();
97
98    write!(&mut msg.file, "\r\n").map_err(|e| Error::Custom(e.into()))?;
99
100    Ok(Status::Continue)
101}
102
103#[on_body(body_callback)]
104fn handle_body(mut ctx: Context<Message>, content: &[u8]) -> milter::Result<Status> {
105    let msg = ctx.data.borrow_mut().unwrap();
106
107    if msg.file.write_all(content).is_err() {
108        cleanup(ctx)?;
109        return Ok(Status::Tempfail);
110    }
111
112    Ok(Status::Continue)
113}
114
115#[on_eom(eom_callback)]
116fn handle_eom(mut ctx: Context<Message>) -> milter::Result<Status> {
117    let msg = ctx.data.take()?.unwrap();
118
119    let path = msg.path.to_str().expect("invalid characters in path");
120
121    ctx.api.add_header("X-Archived", path)?;
122
123    Ok(Status::Continue)
124}
125
126#[on_abort(abort_callback)]
127fn handle_abort(ctx: Context<Message>) -> milter::Result<Status> {
128    cleanup(ctx)?;
129
130    Ok(Status::Continue)
131}
132
133fn cleanup(mut ctx: Context<Message>) -> milter::Result<()> {
134    if let Some(msg) = ctx.data.take()? {
135        fs::remove_file(msg.path).map_err(|e| Error::Custom(e.into()))?;
136    }
137    Ok(())
138}
139
140#[on_close(close_callback)]
141fn handle_close(_: Context<Message>) -> Status {
142    Status::Accept
143}
144
145#[on_unknown(unknown_callback)]
146fn handle_unknown(_: Context<Message>, _: &str) -> Status {
147    Status::Continue
148}
149
150fn main() {
151    let args = env::args().collect::<Vec<_>>();
152
153    if args.len() != 3 || args[1] != "-p" {
154        eprintln!("usage: {} -p <socket>", args[0]);
155        process::exit(1);
156    }
157
158    if let Err(e) = Milter::new(&args[2])
159        .name("SampleFilter")
160        .on_negotiate(negotiate_callback)
161        .on_mail(mail_callback)
162        .on_data(data_callback)
163        .on_header(header_callback)
164        .on_eoh(eoh_callback)
165        .on_body(body_callback)
166        .on_eom(eom_callback)
167        .on_abort(abort_callback)
168        .on_close(close_callback)
169        .on_unknown(unknown_callback)
170        .actions(Actions::ADD_HEADER)
171        .run()
172    {
173        eprintln!("milter execution failed: {}", e);
174        process::exit(1);
175    }
176}