1use anyhow::{bail, ensure, Context, Result};
4use std::time::Duration;
5use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
6
7pub const MAX_PACKET: usize = 256 * 1024;
10pub const MAX_BODY: usize = 1024 * 1024;
12pub const MAX_HEADERS: usize = 512;
14pub const IO_TIMEOUT: Duration = Duration::from_secs(30);
15pub const VERSION: u32 = 6;
16
17pub const C_ABORT: u8 = b'A';
19pub const C_BODY: u8 = b'B';
20pub const C_CONNECT: u8 = b'C';
21pub const C_MACRO: u8 = b'D';
22pub const C_BODYEOB: u8 = b'E';
23pub const C_HELO: u8 = b'H';
24pub const C_QUIT_NC: u8 = b'K';
25pub const C_HEADER: u8 = b'L';
26pub const C_MAIL: u8 = b'M';
27pub const C_EOH: u8 = b'N';
28pub const C_OPTNEG: u8 = b'O';
29pub const C_QUIT: u8 = b'Q';
30pub const C_RCPT: u8 = b'R';
31pub const C_DATA: u8 = b'T';
32pub const C_UNKNOWN: u8 = b'U';
33
34pub const R_ADDRCPT: u8 = b'+';
36pub const R_DELRCPT: u8 = b'-';
37pub const R_ACCEPT: u8 = b'a';
38pub const R_REPLBODY: u8 = b'b';
39pub const R_CONTINUE: u8 = b'c';
40pub const R_DISCARD: u8 = b'd';
41pub const R_ADDHEADER: u8 = b'h';
42pub const R_CHGHEADER: u8 = b'm';
43pub const R_PROGRESS: u8 = b'p';
44pub const R_QUARANTINE: u8 = b'q';
45pub const R_REJECT: u8 = b'r';
46pub const R_TEMPFAIL: u8 = b't';
47pub const R_REPLYCODE: u8 = b'y';
48pub const R_SKIP: u8 = b's';
49
50pub const F_ADDHDRS: u32 = 0x01;
52pub const F_CHGBODY: u32 = 0x02;
53pub const F_ADDRCPT: u32 = 0x04;
54pub const F_DELRCPT: u32 = 0x08;
55pub const F_CHGHDRS: u32 = 0x10;
56pub const F_QUARANTINE: u32 = 0x20;
57pub const ALL_ACTIONS: u32 =
58 F_ADDHDRS | F_CHGBODY | F_ADDRCPT | F_DELRCPT | F_CHGHDRS | F_QUARANTINE;
59
60pub const P_NOCONNECT: u32 = 0x01;
63pub const P_NOHELO: u32 = 0x02;
64pub const P_NOMAIL: u32 = 0x04;
65pub const P_NORCPT: u32 = 0x08;
66pub const P_NOBODY: u32 = 0x10;
67pub const P_NOHDRS: u32 = 0x20;
68pub const P_NOEOH: u32 = 0x40;
69pub const P_NR_HDR: u32 = 0x80;
70pub const P_NODATA: u32 = 0x200;
71pub const P_SKIP: u32 = 0x400;
72pub const P_NR_CONN: u32 = 0x1000;
73pub const P_NR_HELO: u32 = 0x2000;
74pub const P_NR_MAIL: u32 = 0x4000;
75pub const P_NR_RCPT: u32 = 0x8000;
76pub const P_NR_DATA: u32 = 0x10000;
77pub const P_NR_EOH: u32 = 0x40000;
78pub const P_NR_BODY: u32 = 0x80000;
79pub const PROTOCOL_STEPS: [(u32, &str); 17] = [
81 (P_NOCONNECT, "no_connect"),
82 (P_NOHELO, "no_helo"),
83 (P_NOMAIL, "no_mail"),
84 (P_NORCPT, "no_rcpt"),
85 (P_NOBODY, "no_body"),
86 (P_NOHDRS, "no_headers"),
87 (P_NOEOH, "no_eoh"),
88 (P_NR_HDR, "no_reply_header"),
89 (P_NODATA, "no_data"),
90 (P_SKIP, "skip"),
91 (P_NR_CONN, "no_reply_connect"),
92 (P_NR_HELO, "no_reply_helo"),
93 (P_NR_MAIL, "no_reply_mail"),
94 (P_NR_RCPT, "no_reply_rcpt"),
95 (P_NR_DATA, "no_reply_data"),
96 (P_NR_EOH, "no_reply_eoh"),
97 (P_NR_BODY, "no_reply_body"),
98];
99pub const OFFERED_PROTOCOL: u32 = P_NOCONNECT
100 | P_NOHELO
101 | P_NOMAIL
102 | P_NORCPT
103 | P_NOBODY
104 | P_NOHDRS
105 | P_NOEOH
106 | P_NR_HDR
107 | P_NODATA
108 | P_SKIP
109 | P_NR_CONN
110 | P_NR_HELO
111 | P_NR_MAIL
112 | P_NR_RCPT
113 | P_NR_DATA
114 | P_NR_EOH
115 | P_NR_BODY;
116
117pub async fn read_packet<R: AsyncRead + Unpin>(
118 r: &mut R,
119 idle: Duration,
120) -> Result<Option<(u8, Vec<u8>)>> {
121 let mut len = [0u8; 4];
122 match tokio::time::timeout(idle, r.read(&mut len[..1])).await {
123 Err(_) => bail!("peer idle past its deadline"),
124 Ok(Ok(0)) => return Ok(None),
125 Ok(Ok(_)) => {}
126 Ok(Err(e)) => return Err(e.into()),
127 }
128 tokio::time::timeout(IO_TIMEOUT, async {
129 r.read_exact(&mut len[1..]).await?;
130 let n = u32::from_be_bytes(len) as usize;
131 ensure!(
132 (1..=MAX_PACKET).contains(&n),
133 "packet length {n} out of range"
134 );
135 let mut buf = vec![0u8; n];
136 r.read_exact(&mut buf).await?;
137 let cmd = buf.remove(0);
138 Ok(Some((cmd, buf)))
139 })
140 .await
141 .context("packet not completed in time")?
142}
143
144pub fn packet(cmd: u8, data: &[u8]) -> Vec<u8> {
145 let mut out = ((data.len() + 1) as u32).to_be_bytes().to_vec();
146 out.push(cmd);
147 out.extend(data);
148 out
149}
150
151pub async fn write<W: AsyncWrite + Unpin>(w: &mut W, cmd: u8, data: &[u8]) -> Result<()> {
152 tokio::time::timeout(IO_TIMEOUT, w.write_all(&packet(cmd, data)))
153 .await
154 .context("write deadline")??;
155 Ok(())
156}
157
158pub fn strings(data: &[u8]) -> Vec<String> {
160 let mut parts: Vec<String> = data
161 .split(|b| *b == 0)
162 .map(|s| String::from_utf8_lossy(s).into_owned())
163 .collect();
164 if data.last() == Some(&0) {
165 parts.pop();
166 }
167 parts
168}
169
170pub fn cstrings(fields: &[&str]) -> Vec<u8> {
171 let mut out = Vec::new();
172 for f in fields {
173 out.extend(f.as_bytes());
174 out.push(0);
175 }
176 out
177}
178
179pub fn check_field(s: &str, what: &str) -> Result<()> {
181 ensure!(
182 s.len() <= 4096 && !s.contains(['\0', '\r', '\n']),
183 "{what} must be one line without NUL, at most 4096 bytes"
184 );
185 Ok(())
186}
187
188pub fn optneg(version: u32, actions: u32, protocol: u32) -> Vec<u8> {
190 let mut out = version.to_be_bytes().to_vec();
191 out.extend(actions.to_be_bytes());
192 out.extend(protocol.to_be_bytes());
193 out
194}
195
196pub fn parse_optneg(d: &[u8]) -> Result<(u32, u32, u32)> {
197 ensure!(d.len() >= 12, "short OPTNEG");
198 let n = |i: usize| u32::from_be_bytes(d[i..i + 4].try_into().unwrap());
199 Ok((n(0), n(4), n(8)))
200}
201
202pub fn parse_connect(d: &[u8]) -> Result<(String, char, u16, String)> {
204 let nul = d
205 .iter()
206 .position(|b| *b == 0)
207 .context("CONNECT without a hostname")?;
208 let host = String::from_utf8_lossy(&d[..nul]).into_owned();
209 let rest = &d[nul + 1..];
210 let family = *rest.first().context("CONNECT without a family")? as char;
211 if family == 'U' || rest.len() < 3 {
212 return Ok((host, family, 0, String::new()));
213 }
214 let port = u16::from_be_bytes([rest[1], rest[2]]);
215 let addr = strings(&rest[3..]).into_iter().next().unwrap_or_default();
216 Ok((host, family, port, addr))
217}
218
219pub fn connect_body(host: &str, family: char, port: u16, addr: &str) -> Vec<u8> {
220 let mut out = cstrings(&[host]);
221 out.push(family as u8);
222 out.extend(port.to_be_bytes());
223 out.extend(cstrings(&[addr]));
224 out
225}
226
227pub fn reply_name(code: u8) -> &'static str {
229 match code {
230 R_ACCEPT => "accept",
231 R_CONTINUE => "continue",
232 R_DISCARD => "discard",
233 R_REJECT => "reject",
234 R_TEMPFAIL => "tempfail",
235 R_REPLYCODE => "replycode",
236 R_ADDHEADER => "add_header",
237 R_CHGHEADER => "change_header",
238 R_ADDRCPT => "add_rcpt",
239 R_DELRCPT => "del_rcpt",
240 R_REPLBODY => "replace_body",
241 R_QUARANTINE => "quarantine",
242 R_PROGRESS => "progress",
243 R_SKIP => "skip",
244 _ => "unknown",
245 }
246}
247
248pub fn replycode(code: u64, xcode: Option<&str>, text: &str) -> Result<Vec<u8>> {
250 ensure!((400..600).contains(&code), "reply code must be 4xx or 5xx");
251 check_field(text, "text")?;
252 let line = match xcode {
253 Some(x) => {
254 check_field(x, "xcode")?;
255 ensure!(
256 x.starts_with(if code < 500 { '4' } else { '5' }),
257 "xcode class must match the code"
258 );
259 format!("{code} {x} {text}")
260 }
261 None => format!("{code} {text}"),
262 };
263 Ok(cstrings(&[&line]))
264}