netget/server/lmtp/
wire.rs1use anyhow::{bail, Context, Result};
8use serde_json::{json, Map, Value};
9use std::time::Duration;
10use tokio::io::{AsyncBufRead, AsyncBufReadExt};
11
12pub const MAX_LINE_BYTES: usize = 1000;
14pub const MAX_RECIPIENTS: usize = 100;
16pub const DEFAULT_MAX_MESSAGE_BYTES: u64 = 10 * 1024 * 1024;
18pub const MAX_MESSAGE_BYTES_LIMIT: u64 = 64 * 1024 * 1024;
20pub const MAX_BODY_FOR_HANDLER: usize = 64 * 1024;
22pub const MAX_HEADERS: usize = 100;
24pub const MAX_REPLY_LINES: usize = 64;
26pub const IO_TIMEOUT: Duration = Duration::from_secs(30);
28
29pub enum Line {
31 Text(String),
32 TooLong,
33 Eof,
34}
35
36pub async fn read_line<R: AsyncBufRead + Unpin>(
39 reader: &mut R,
40 deadline: Duration,
41) -> Result<Line> {
42 tokio::time::timeout(deadline, read_line_inner(reader))
43 .await
44 .context("LMTP read deadline")?
45}
46
47async fn read_line_inner<R: AsyncBufRead + Unpin>(reader: &mut R) -> Result<Line> {
48 let mut line = Vec::new();
49 let mut overflow = false;
50 loop {
51 let available = reader.fill_buf().await?;
52 if available.is_empty() {
53 if line.is_empty() && !overflow {
54 return Ok(Line::Eof);
55 }
56 bail!("LMTP peer closed mid-line");
57 }
58 let (chunk, found) = match available.iter().position(|b| *b == b'\n') {
59 Some(i) => (&available[..=i], true),
60 None => (available, false),
61 };
62 let taken = chunk.len();
63 if !overflow {
64 if line.len() + taken > MAX_LINE_BYTES {
65 overflow = true;
66 line.clear();
67 } else {
68 line.extend_from_slice(chunk);
69 }
70 }
71 reader.consume(taken);
72 if found {
73 if overflow {
74 return Ok(Line::TooLong);
75 }
76 while matches!(line.last(), Some(b'\n' | b'\r')) {
77 line.pop();
78 }
79 return Ok(Line::Text(String::from_utf8_lossy(&line).into_owned()));
80 }
81 }
82}
83
84pub fn render(code: u16, enhanced: &str, text: &str) -> String {
86 let text = sanitize(text);
87 if enhanced.is_empty() {
88 format!("{code} {text}\r\n")
89 } else {
90 format!("{code} {enhanced} {text}\r\n")
91 }
92}
93
94pub fn render_multi(code: u16, lines: &[String]) -> String {
95 let mut out = String::new();
96 for (i, line) in lines.iter().enumerate() {
97 let sep = if i + 1 == lines.len() { ' ' } else { '-' };
98 out.push_str(&format!("{code}{sep}{}\r\n", sanitize(line)));
99 }
100 out
101}
102
103pub fn sanitize(text: &str) -> String {
105 let cleaned = crate::utils::sanitize::line_field(text);
106 let trimmed = cleaned.trim();
107 let mut out = String::new();
108 for c in trimmed.chars() {
109 if out.len() + c.len_utf8() > 400 {
110 break;
111 }
112 out.push(c);
113 }
114 out
115}
116
117#[derive(Debug, Clone)]
119pub struct Reply {
120 pub code: u16,
121 pub lines: Vec<String>,
122}
123
124impl Reply {
125 pub fn text(&self) -> String {
126 self.lines.join(" ")
127 }
128 pub fn positive(&self) -> bool {
129 (200..400).contains(&self.code)
130 }
131 pub fn to_json(&self) -> Value {
132 json!({"code": self.code, "text": self.text()})
133 }
134}
135
136pub async fn read_reply<R: AsyncBufRead + Unpin>(reader: &mut R) -> Result<Reply> {
138 let mut lines = Vec::new();
139 let mut code = None;
140 loop {
141 let line = match read_line(reader, IO_TIMEOUT).await? {
142 Line::Text(line) => line,
143 Line::TooLong => bail!("LMTP reply line exceeds {MAX_LINE_BYTES} bytes"),
144 Line::Eof => bail!("LMTP server closed the connection before replying"),
145 };
146 let bytes = line.as_bytes();
147 if bytes.len() < 3 || !bytes[..3].iter().all(u8::is_ascii_digit) {
148 bail!("Malformed LMTP reply line");
149 }
150 let this: u16 = line[..3].parse()?;
151 if let Some(previous) = code {
152 if previous != this {
153 bail!("LMTP multi-line reply changed code from {previous} to {this}");
154 }
155 }
156 code = Some(this);
157 let more = bytes.get(3) == Some(&b'-');
158 if bytes.len() > 3 && !matches!(bytes[3], b' ' | b'-') {
159 bail!("Malformed LMTP reply separator");
160 }
161 lines.push(line.get(4..).unwrap_or("").to_string());
162 if lines.len() > MAX_REPLY_LINES {
163 bail!("LMTP reply exceeds {MAX_REPLY_LINES} lines");
164 }
165 if !more {
166 return Ok(Reply { code: this, lines });
167 }
168 }
169}
170
171pub fn parse_path(argument: &str, keyword: &str) -> Option<(String, Vec<String>)> {
173 let argument = argument.trim_start();
174 if argument.len() < keyword.len() || !argument[..keyword.len()].eq_ignore_ascii_case(keyword) {
175 return None;
176 }
177 let rest = argument[keyword.len()..].trim_start();
178 let rest = rest.strip_prefix('<')?;
179 let end = rest.find('>')?;
180 let address = rest[..end].trim().to_string();
181 if address.len() > 256
182 || crate::utils::sanitize::has_controls(&address)
183 || address.contains(' ')
184 {
185 return None;
186 }
187 let params = rest[end + 1..]
188 .split_whitespace()
189 .map(str::to_string)
190 .collect();
191 Some((address, params))
192}
193
194pub fn valid_mailbox(address: &str, allow_empty: bool) -> bool {
196 (allow_empty || !address.is_empty())
197 && address.len() <= 256
198 && !crate::utils::sanitize::has_controls(address)
199 && !address.contains([' ', '<', '>'])
200}
201
202pub fn summarize(raw: &[u8]) -> (Map<String, Value>, String, bool) {
204 let text = String::from_utf8_lossy(raw);
205 let (head, body) = match text.find("\r\n\r\n") {
206 Some(i) => (&text[..i], &text[i + 4..]),
207 None => match text.find("\n\n") {
208 Some(i) => (&text[..i], &text[i + 2..]),
209 None => (text.as_ref(), ""),
210 },
211 };
212 let mut headers = Map::new();
213 let mut current: Option<(String, String)> = None;
214 let flush = |current: &mut Option<(String, String)>, headers: &mut Map<String, Value>| {
215 if let Some((name, value)) = current.take() {
216 if headers.len() < MAX_HEADERS && !headers.contains_key(&name) {
217 headers.insert(name, Value::String(value.trim().to_string()));
218 }
219 }
220 };
221 for line in head.lines() {
222 if line.starts_with(' ') || line.starts_with('\t') {
223 if let Some((_, value)) = current.as_mut() {
224 value.push(' ');
225 value.push_str(line.trim());
226 }
227 continue;
228 }
229 flush(&mut current, &mut headers);
230 if let Some((name, value)) = line.split_once(':') {
231 current = Some((name.trim().to_ascii_lowercase(), value.to_string()));
232 }
233 }
234 flush(&mut current, &mut headers);
235 let truncated = body.len() > MAX_BODY_FOR_HANDLER;
236 let mut end = body.len().min(MAX_BODY_FOR_HANDLER);
237 while !body.is_char_boundary(end) {
238 end -= 1;
239 }
240 (headers, body[..end].to_string(), truncated)
241}
242
243pub fn dot_stuff(message: &str) -> Vec<u8> {
245 let mut out = Vec::with_capacity(message.len() + 8);
246 for line in message.split('\n') {
247 let line = line.strip_suffix('\r').unwrap_or(line);
248 if line.starts_with('.') {
249 out.push(b'.');
250 }
251 out.extend_from_slice(line.as_bytes());
252 out.extend_from_slice(b"\r\n");
253 }
254 if message.ends_with('\n') {
256 out.truncate(out.len() - 2);
257 }
258 out.extend_from_slice(b".\r\n");
259 out
260}