1pub mod actions;
5pub mod wire;
6
7use crate::llm::action_helper::call_llm;
8use crate::llm::actions::protocol_trait::ActionResult;
9use crate::logging::emit::Log;
10use crate::protocol::{Event, SpawnContext};
11use crate::server::connection::ConnectionId;
12use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
13use anyhow::{ensure, Result};
14use serde_json::{json, Map, Value};
15use std::{net::SocketAddr, time::Duration};
16use tokio::net::{TcpListener, TcpStream};
17
18pub const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
19pub const MAX_IDLE_TIMEOUT_SECS: u64 = 86400;
20pub const MAX_MACROS: usize = 256;
22
23pub async fn spawn(ctx: SpawnContext) -> Result<SocketAddr> {
24 let secs = ctx
25 .startup_params
26 .as_ref()
27 .map(|p| p.get_optional_u64("idle_timeout_secs"))
28 .transpose()?
29 .flatten()
30 .unwrap_or(IDLE_TIMEOUT.as_secs());
31 ensure!(
32 (1..=MAX_IDLE_TIMEOUT_SECS).contains(&secs),
33 "idle_timeout_secs must be between 1 and {MAX_IDLE_TIMEOUT_SECS}"
34 );
35 let idle = Duration::from_secs(secs);
36 let listener = TcpListener::bind(ctx.legacy_listen_addr()).await?;
37 let addr = listener.local_addr()?;
38 Log::new(Some(&ctx.status_tx)).info(format!("Milter listening on {addr}"));
39 let state = ctx.state.clone();
40 let server_id = ctx.server_id;
41 let accept = tokio::spawn(async move {
42 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(
43 crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS,
44 );
45 loop {
46 let (socket, peer, permit) = match crate::server::accept_bounded::accept_bounded(
47 &listener,
48 &limiter,
49 b"",
50 "Milter",
51 Some(&ctx.status_tx),
52 )
53 .await
54 {
55 Ok(v) => v,
56 Err(_) => break,
57 };
58 let id = ConnectionId::new(ctx.state.get_next_unified_id().await);
59 let now = crate::utils::clock::Instant::now();
60 ctx.state
61 .add_connection_to_server(
62 server_id,
63 ConnectionState {
64 id,
65 remote_addr: peer,
66 local_addr: addr,
67 bytes_sent: 0,
68 bytes_received: 0,
69 packets_sent: 0,
70 packets_received: 0,
71 last_activity: now,
72 status: ConnectionStatus::Active,
73 status_changed_at: now,
74 protocol_info: ProtocolConnectionInfo::empty(),
75 },
76 )
77 .await;
78 let child = ctx.clone();
79 ctx.state
80 .spawn_server_task(server_id, async move {
81 let _permit = permit;
82 if let Err(e) = session(&child, id, socket, idle).await {
83 Log::new(Some(&child.status_tx))
84 .warn(format!("Milter connection {id} ended: {e:#}"));
85 }
86 child
87 .state
88 .update_connection_status(server_id, id, ConnectionStatus::Closed)
89 .await;
90 let _ = child.status_tx.send("__UPDATE_UI__".into());
91 })
92 .await;
93 }
94 });
95 state.register_server_task(server_id, accept).await;
96 Ok(addr)
97}
98
99#[derive(Default)]
100struct Message {
101 sender: String,
102 recipients: Vec<String>,
103 headers: Vec<Value>,
104 body: Vec<u8>,
105 truncated: bool,
106}
107
108struct Session {
109 actions: u32,
110 macros: Map<String, Value>,
111 connection: Value,
112 message: Message,
113}
114
115fn outcome(ctx: &SpawnContext, id: ConnectionId, stage: &str, decision: &str) {
116 let line = format!("Milter connection {id} stage={stage} decision={decision}");
117 let log = Log::new(Some(&ctx.status_tx));
118 if decision.starts_with("fail_closed") {
119 log.error(line)
120 } else {
121 log.info(line)
122 }
123}
124
125async fn ask(
127 ctx: &SpawnContext,
128 id: ConnectionId,
129 event: Event,
130 stage: &str,
131) -> Option<Vec<Value>> {
132 match call_llm(
133 &ctx.llm_client,
134 &ctx.state,
135 ctx.server_id,
136 Some(id),
137 &event,
138 &actions::MilterProtocol,
139 )
140 .await
141 {
142 Ok(r) if r.failures.is_empty() => {
143 let mut out = Vec::new();
144 let mut stack = r.protocol_results;
145 while let Some(item) = stack.pop() {
146 match item {
147 ActionResult::Custom { name, data } if name.starts_with("milter_") => {
148 out.push(data)
149 }
150 ActionResult::Multiple(items) => stack.extend(items),
151 _ => {}
152 }
153 }
154 out.reverse();
155 Some(out)
156 }
157 Ok(_) => {
158 outcome(ctx, id, stage, "fail_closed_invalid_reply");
159 None
160 }
161 Err(_) => {
162 outcome(ctx, id, stage, "fail_closed_llm_error");
163 None
164 }
165 }
166}
167
168const DECISIONS: [&str; 6] = [
169 "milter_continue",
170 "milter_accept",
171 "milter_reject",
172 "milter_tempfail",
173 "milter_discard",
174 "milter_reply",
175];
176
177async fn answer<W: tokio::io::AsyncWrite + Unpin>(
180 ctx: &SpawnContext,
181 id: ConnectionId,
182 w: &mut W,
183 s: &Session,
184 stage: &str,
185 answers: Option<Vec<Value>>,
186 end_of_message: bool,
187) -> Result<&'static str> {
188 let Some(answers) = answers else {
189 wire::write(w, wire::R_TEMPFAIL, &[]).await?;
190 return Ok("tempfail");
191 };
192 let log = Log::new(Some(&ctx.status_tx));
193 let mut decision = None;
194 for a in &answers {
195 let name = a["type"].as_str().unwrap_or_default();
196 if DECISIONS.contains(&name) {
197 decision = Some(a.clone());
198 continue;
199 }
200 if !end_of_message {
201 log.warn(format!(
202 "Milter: {name} is only possible at end of message; ignored at {stage}"
203 ));
204 continue;
205 }
206 let needed = match name {
207 "milter_add_header" => wire::F_ADDHDRS,
208 "milter_change_header" => wire::F_CHGHDRS,
209 "milter_add_rcpt" => wire::F_ADDRCPT,
210 "milter_del_rcpt" => wire::F_DELRCPT,
211 "milter_replace_body" => wire::F_CHGBODY,
212 "milter_quarantine" => wire::F_QUARANTINE,
213 _ => 0,
214 };
215 if s.actions & needed == 0 {
216 log.warn(format!("Milter: the MTA did not allow {name}; not sent"));
217 continue;
218 }
219 let (cmd, data) = actions::reply_packet(a)?;
220 wire::write(w, cmd, &data).await?;
221 }
222 let (cmd, data) = match &decision {
223 Some(d) => actions::reply_packet(d)?,
224 None if end_of_message => (wire::R_ACCEPT, vec![]),
226 None => (wire::R_CONTINUE, vec![]),
227 };
228 wire::write(w, cmd, &data).await?;
229 let name = wire::reply_name(cmd);
230 outcome(
231 ctx,
232 id,
233 stage,
234 if decision.is_some() {
235 "model_answer"
236 } else {
237 "model_silent_continue"
238 },
239 );
240 Ok(name)
241}
242
243async fn session(
244 ctx: &SpawnContext,
245 id: ConnectionId,
246 socket: TcpStream,
247 idle: Duration,
248) -> Result<()> {
249 let (mut r, mut w) = tokio::io::split(socket);
250 let mut s = Session {
251 actions: 0,
252 macros: Map::new(),
253 connection: json!({}),
254 message: Message::default(),
255 };
256 while let Some((cmd, data)) = wire::read_packet(&mut r, idle).await? {
257 ctx.state
258 .update_connection_stats(
259 ctx.server_id,
260 id,
261 Some(data.len() as u64 + 5),
262 None,
263 Some(1),
264 None,
265 )
266 .await;
267 let stage_event = |e: &'static std::sync::LazyLock<crate::protocol::EventType>,
268 mut v: Value,
269 s: &Session| {
270 v["connection"] = s.connection.clone();
271 v["macros"] = Value::Object(s.macros.clone());
272 Event::new(e, v)
273 };
274 match cmd {
275 wire::C_OPTNEG => {
276 let (version, actions, _protocol) = wire::parse_optneg(&data)?;
277 ensure!(version >= 2, "milter protocol version {version} is too old");
278 s.actions = actions & wire::ALL_ACTIONS;
279 wire::write(
280 &mut w,
281 wire::C_OPTNEG,
282 &wire::optneg(version.min(wire::VERSION), s.actions, 0),
283 )
284 .await?;
285 }
286 wire::C_MACRO => {
287 let fields = wire::strings(data.get(1..).unwrap_or_default());
288 for pair in fields.chunks(2) {
289 if let [k, v] = pair {
290 if s.macros.len() < MAX_MACROS || s.macros.contains_key(k) {
291 s.macros.insert(k.clone(), json!(v));
292 }
293 }
294 }
295 }
296 wire::C_CONNECT => {
297 let (hostname, family, port, address) = wire::parse_connect(&data)?;
298 s.connection = json!({"hostname": hostname, "family": family.to_string(), "address": address, "port": port});
299 let e = stage_event(
300 &actions::CONNECT_EVENT,
301 json!({"hostname": hostname, "address": address, "port": port}),
302 &s,
303 );
304 let a = ask(ctx, id, e, "connect").await;
305 answer(ctx, id, &mut w, &s, "connect", a, false).await?;
306 }
307 wire::C_HELO => {
308 let helo = wire::strings(&data).into_iter().next().unwrap_or_default();
309 s.connection["helo"] = json!(helo);
310 let e = stage_event(&actions::HELO_EVENT, json!({"helo": helo}), &s);
311 let a = ask(ctx, id, e, "helo").await;
312 answer(ctx, id, &mut w, &s, "helo", a, false).await?;
313 }
314 wire::C_MAIL => {
315 let mut f = wire::strings(&data);
316 ensure!(!f.is_empty(), "MAIL without a sender");
317 let sender = f.remove(0);
318 s.message = Message {
319 sender: sender.clone(),
320 ..Message::default()
321 };
322 let e = stage_event(
323 &actions::MAIL_EVENT,
324 json!({"sender": sender, "esmtp_args": f}),
325 &s,
326 );
327 let a = ask(ctx, id, e, "mail").await;
328 answer(ctx, id, &mut w, &s, "mail", a, false).await?;
329 }
330 wire::C_RCPT => {
331 let mut f = wire::strings(&data);
332 ensure!(!f.is_empty(), "RCPT without a recipient");
333 let rcpt = f.remove(0);
334 let e = stage_event(
335 &actions::RCPT_EVENT,
336 json!({"recipient": rcpt, "esmtp_args": f}),
337 &s,
338 );
339 let a = ask(ctx, id, e, "rcpt").await;
340 if answer(ctx, id, &mut w, &s, "rcpt", a, false).await? == "continue" {
341 s.message.recipients.push(rcpt);
342 }
343 }
344 wire::C_HEADER => {
345 let f = wire::strings(&data);
346 if s.message.headers.len() < wire::MAX_HEADERS {
347 s.message.headers.push(json!({"name": f.first().cloned().unwrap_or_default(), "value": f.get(1).cloned().unwrap_or_default()}));
348 }
349 wire::write(&mut w, wire::R_CONTINUE, &[]).await?;
350 }
351 wire::C_BODY => {
352 let room = wire::MAX_BODY.saturating_sub(s.message.body.len());
353 s.message.truncated |= data.len() > room;
354 s.message
355 .body
356 .extend_from_slice(&data[..data.len().min(room)]);
357 wire::write(&mut w, wire::R_CONTINUE, &[]).await?;
358 }
359 wire::C_BODYEOB => {
360 if !data.is_empty() {
361 let room = wire::MAX_BODY.saturating_sub(s.message.body.len());
362 s.message.truncated |= data.len() > room;
363 s.message
364 .body
365 .extend_from_slice(&data[..data.len().min(room)]);
366 }
367 let m = &s.message;
368 let e = stage_event(
369 &actions::MESSAGE_EVENT,
370 json!({"sender": m.sender, "recipients": m.recipients, "headers": m.headers,
371 "body": String::from_utf8_lossy(&m.body), "body_truncated": m.truncated}),
372 &s,
373 );
374 let a = ask(ctx, id, e, "message").await;
375 answer(ctx, id, &mut w, &s, "message", a, true).await?;
376 s.message = Message::default();
377 }
378 wire::C_DATA | wire::C_EOH | wire::C_UNKNOWN => {
379 wire::write(&mut w, wire::R_CONTINUE, &[]).await?
380 }
381 wire::C_ABORT => s.message = Message::default(),
382 wire::C_QUIT_NC => {
383 s.message = Message::default();
384 s.macros.clear();
385 s.connection = json!({});
386 }
387 wire::C_QUIT => return Ok(()),
388 other => anyhow::bail!("unknown milter command {:?}", other as char),
389 }
390 }
391 Ok(())
392}