1use crate::Command;
2use serde::{Deserialize, Serialize};
3use std::time::Duration;
4
5#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
6pub struct SmtpClientTimeouts {
7 #[serde(
8 default = "SmtpClientTimeouts::default_connect_timeout",
9 with = "duration_serde"
10 )]
11 pub connect_timeout: Duration,
12
13 #[serde(
14 default = "SmtpClientTimeouts::default_banner_timeout",
15 with = "duration_serde"
16 )]
17 pub banner_timeout: Duration,
18
19 #[serde(
20 default = "SmtpClientTimeouts::default_ehlo_timeout",
21 with = "duration_serde"
22 )]
23 pub ehlo_timeout: Duration,
24
25 #[serde(
26 default = "SmtpClientTimeouts::default_mail_from_timeout",
27 with = "duration_serde"
28 )]
29 pub mail_from_timeout: Duration,
30
31 #[serde(
32 default = "SmtpClientTimeouts::default_rcpt_to_timeout",
33 with = "duration_serde"
34 )]
35 pub rcpt_to_timeout: Duration,
36
37 #[serde(
38 default = "SmtpClientTimeouts::default_data_timeout",
39 with = "duration_serde"
40 )]
41 pub data_timeout: Duration,
42 #[serde(
43 default = "SmtpClientTimeouts::default_data_dot_timeout",
44 with = "duration_serde"
45 )]
46 pub data_dot_timeout: Duration,
47 #[serde(
48 default = "SmtpClientTimeouts::default_rset_timeout",
49 with = "duration_serde"
50 )]
51 pub rset_timeout: Duration,
52
53 #[serde(
54 default = "SmtpClientTimeouts::default_idle_timeout",
55 with = "duration_serde"
56 )]
57 pub idle_timeout: Duration,
58
59 #[serde(
60 default = "SmtpClientTimeouts::default_starttls_timeout",
61 with = "duration_serde"
62 )]
63 pub starttls_timeout: Duration,
64
65 #[serde(
66 default = "SmtpClientTimeouts::default_auth_timeout",
67 with = "duration_serde"
68 )]
69 pub auth_timeout: Duration,
70}
71
72impl Default for SmtpClientTimeouts {
73 fn default() -> Self {
74 Self {
75 connect_timeout: Self::default_connect_timeout(),
76 banner_timeout: Self::default_banner_timeout(),
77 ehlo_timeout: Self::default_ehlo_timeout(),
78 mail_from_timeout: Self::default_mail_from_timeout(),
79 rcpt_to_timeout: Self::default_rcpt_to_timeout(),
80 data_timeout: Self::default_data_timeout(),
81 data_dot_timeout: Self::default_data_dot_timeout(),
82 rset_timeout: Self::default_rset_timeout(),
83 idle_timeout: Self::default_idle_timeout(),
84 starttls_timeout: Self::default_starttls_timeout(),
85 auth_timeout: Self::default_auth_timeout(),
86 }
87 }
88}
89
90impl SmtpClientTimeouts {
91 fn default_connect_timeout() -> Duration {
92 Duration::from_secs(60)
93 }
94 fn default_banner_timeout() -> Duration {
95 Duration::from_secs(60)
96 }
97 fn default_auth_timeout() -> Duration {
98 Duration::from_secs(60)
99 }
100 fn default_ehlo_timeout() -> Duration {
101 Duration::from_secs(300)
102 }
103 fn default_mail_from_timeout() -> Duration {
104 Duration::from_secs(300)
105 }
106 fn default_rcpt_to_timeout() -> Duration {
107 Duration::from_secs(300)
108 }
109 fn default_data_timeout() -> Duration {
110 Duration::from_secs(300)
111 }
112 fn default_data_dot_timeout() -> Duration {
113 Duration::from_secs(300)
114 }
115 fn default_rset_timeout() -> Duration {
116 Duration::from_secs(5)
117 }
118 fn default_idle_timeout() -> Duration {
119 Duration::from_secs(5)
120 }
121 fn default_starttls_timeout() -> Duration {
122 Duration::from_secs(5)
123 }
124
125 pub fn short_timeouts() -> Self {
126 let short = Duration::from_secs(20);
127 Self {
128 connect_timeout: short,
129 banner_timeout: short,
130 ehlo_timeout: short,
131 mail_from_timeout: short,
132 rcpt_to_timeout: short,
133 data_timeout: short,
134 data_dot_timeout: short,
135 rset_timeout: short,
136 idle_timeout: short,
137 starttls_timeout: short,
138 auth_timeout: short,
139 }
140 }
141
142 pub fn total_connection_lifetime_duration(&self) -> Duration {
145 self.connect_timeout
146 + self.banner_timeout
147 + self.ehlo_timeout
148 + self.auth_timeout
149 + self.mail_from_timeout
150 + self.rcpt_to_timeout
151 + self.data_timeout
152 + self.data_dot_timeout
153 + self.starttls_timeout
154 + self.idle_timeout
155 }
156
157 pub fn total_message_send_duration(&self) -> Duration {
160 self.mail_from_timeout + self.rcpt_to_timeout + self.data_timeout + self.data_dot_timeout
161 }
162}
163
164#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Hash)]
165pub struct Response {
166 pub code: u16,
167 pub enhanced_code: Option<EnhancedStatusCode>,
168 #[serde(serialize_with = "as_single_line")]
169 pub content: String,
170 pub command: Option<String>,
171}
172
173impl Response {
174 pub fn to_single_line(&self) -> String {
175 let mut line = format!("{} ", self.code);
176
177 if let Some(enh) = &self.enhanced_code {
178 line.push_str(&format!("{}.{}.{} ", enh.class, enh.subject, enh.detail));
179 }
180
181 line.push_str(&remove_line_break(&self.content));
182
183 line
184 }
185
186 pub fn is_transient(&self) -> bool {
187 self.code >= 400 && self.code < 500
188 }
189
190 pub fn is_permanent(&self) -> bool {
191 self.code >= 500 && self.code < 600
192 }
193
194 pub fn with_code_and_message(code: u16, message: &str) -> Self {
195 let lines: Vec<&str> = message.lines().collect();
196
197 let mut builder = ResponseBuilder::new(&ResponseLine {
198 code,
199 content: lines[0],
200 is_final: lines.len() == 1,
201 });
202
203 for (n, line) in lines.iter().enumerate().skip(1) {
204 builder
205 .add_line(&ResponseLine {
206 code,
207 content: line,
208 is_final: n == lines.len() - 1,
209 })
210 .ok();
211 }
212
213 builder.build(None)
214 }
215
216 pub fn was_due_to_message(&self) -> bool {
228 if let Some(command) = &self.command {
229 if let Ok(cmd) = Command::parse(command) {
230 return match cmd {
231 Command::MailFrom { .. }
232 | Command::RcptTo { .. }
233 | Command::Data
234 | Command::DataDot => true,
235 Command::Ehlo(_)
236 | Command::Helo(_)
237 | Command::Lhlo(_)
238 | Command::Rset
239 | Command::Quit
240 | Command::StartTls
241 | Command::Vrfy(_)
242 | Command::Expn(_)
243 | Command::Noop(_)
244 | Command::Help(_)
245 | Command::Auth { .. } => false,
246 };
247 }
248 }
249
250 true
251 }
252}
253
254#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Hash)]
255pub struct EnhancedStatusCode {
256 pub class: u8,
257 pub subject: u16,
258 pub detail: u16,
259}
260
261fn parse_enhanced_status_code(line: &str) -> Option<(EnhancedStatusCode, &str)> {
262 let mut fields = line.splitn(3, '.');
263 let class = fields.next()?.parse::<u8>().ok()?;
264 if !matches!(class, 2 | 4 | 5) {
265 return None;
267 }
268 let subject = fields.next()?.parse::<u16>().ok()?;
269
270 let remainder = fields.next()?;
271 let mut fields = remainder.splitn(2, ' ');
272 let detail = fields.next()?.parse::<u16>().ok()?;
273 let remainder = fields.next()?;
274
275 Some((
276 EnhancedStatusCode {
277 class,
278 subject,
279 detail,
280 },
281 remainder,
282 ))
283}
284
285fn remove_line_break(data: &str) -> String {
286 let data = data.as_bytes();
287 let mut normalized = Vec::with_capacity(data.len());
288 let mut last_idx = 0;
289
290 for i in memchr::memchr2_iter(b'\r', b'\n', data) {
291 match data[i] {
292 b'\r' => {
293 normalized.extend_from_slice(&data[last_idx..i]);
294 if data.get(i + 1).copied() != Some(b'\n') {
295 normalized.push(b' ');
296 }
297 }
298 b'\n' => {
299 normalized.extend_from_slice(&data[last_idx..i]);
300 normalized.push(b' ');
301 }
302 _ => unreachable!(),
303 }
304 last_idx = i + 1;
305 }
306
307 normalized.extend_from_slice(&data[last_idx..]);
308 unsafe { String::from_utf8_unchecked(normalized) }
313}
314
315#[derive(Debug, PartialEq, Eq)]
316pub(crate) struct ResponseLine<'a> {
317 pub code: u16,
318 pub is_final: bool,
319 pub content: &'a str,
320}
321
322impl ResponseLine<'_> {
323 fn to_original_line(&self) -> String {
325 format!(
326 "{}{}{}",
327 self.code,
328 if self.is_final { " " } else { "-" },
329 self.content
330 )
331 }
332}
333
334pub(crate) struct ResponseBuilder {
335 pub code: u16,
336 pub enhanced_code: Option<EnhancedStatusCode>,
337 pub content: String,
338}
339
340impl ResponseBuilder {
341 pub fn new(parsed: &ResponseLine) -> Self {
342 let code = parsed.code;
343 let (enhanced_code, content) = match parse_enhanced_status_code(parsed.content) {
344 Some((enhanced, content)) => (Some(enhanced), content.to_string()),
345 None => (None, parsed.content.to_string()),
346 };
347
348 Self {
349 code,
350 enhanced_code,
351 content,
352 }
353 }
354
355 pub fn add_line(&mut self, parsed: &ResponseLine) -> Result<(), String> {
356 if parsed.code != self.code {
357 return Err(parsed.to_original_line());
358 }
359
360 self.content.push('\n');
361
362 let mut content = parsed.content;
363
364 if let Some(enh) = &self.enhanced_code {
365 let prefix = format!("{}.{}.{} ", enh.class, enh.subject, enh.detail);
366 if let Some(remainder) = parsed.content.strip_prefix(&prefix) {
367 content = remainder;
368 }
369 }
370
371 self.content.push_str(content);
372 Ok(())
373 }
374
375 pub fn build(self, command: Option<String>) -> Response {
376 Response {
377 code: self.code,
378 content: self.content,
379 enhanced_code: self.enhanced_code,
380 command,
381 }
382 }
383}
384
385#[allow(clippy::ptr_arg)]
386fn as_single_line<S>(content: &String, serializer: S) -> Result<S::Ok, S::Error>
387where
388 S: serde::Serializer,
389{
390 serializer.serialize_str(&remove_line_break(content))
391}
392
393#[cfg(test)]
394mod test {
395 use super::*;
396
397 #[test]
398 fn remove_crlf() {
399 fn remove(s: &str, expect: &str) {
400 assert_eq!(remove_line_break(s), expect, "input: {s:?}");
401 }
402
403 remove("hello\r\nthere\r\n", "hello there ");
404 remove("hello\r", "hello ");
405 remove("hello\nthere\r\n", "hello there ");
406 remove("hello\r\nthere\n", "hello there ");
407 remove("hello\r\r\r\nthere\n", "hello there ");
408 }
409
410 #[test]
411 fn response_parsing() {
412 assert_eq!(
413 parse_enhanced_status_code("2.0.1 w00t"),
414 Some((
415 EnhancedStatusCode {
416 class: 2,
417 subject: 0,
418 detail: 1
419 },
420 "w00t"
421 ))
422 );
423
424 assert_eq!(parse_enhanced_status_code("3.0.0 w00t"), None);
425
426 assert_eq!(parse_enhanced_status_code("2.0.0.1 w00t"), None);
427
428 assert_eq!(parse_enhanced_status_code("2.0.0.1w00t"), None);
429 }
430}