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, Copy, Hash)]
165pub enum IsTooManyRecipients {
166 Yes,
167 No,
168 Maybe,
169}
170
171#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Hash)]
172pub struct Response {
173 pub code: u16,
174 pub enhanced_code: Option<EnhancedStatusCode>,
175 #[serde(serialize_with = "as_single_line")]
176 pub content: String,
177 pub command: Option<String>,
178}
179
180impl Response {
181 pub fn to_single_line(&self) -> String {
182 let mut line = format!("{} ", self.code);
183
184 if let Some(enh) = &self.enhanced_code {
185 line.push_str(&format!("{}.{}.{} ", enh.class, enh.subject, enh.detail));
186 }
187
188 line.push_str(&remove_line_break(&self.content));
189
190 line
191 }
192
193 pub fn is_transient(&self) -> bool {
194 self.code >= 400 && self.code < 500
195 }
196
197 pub fn is_permanent(&self) -> bool {
198 self.code >= 500 && self.code < 600
199 }
200
201 pub fn is_too_many_recipients(&self) -> IsTooManyRecipients {
202 if !self
206 .command
207 .as_deref()
208 .map(|c| c.starts_with("RCPT"))
209 .unwrap_or(false)
210 {
211 return IsTooManyRecipients::No;
212 }
213
214 match (self.code, &self.enhanced_code) {
215 (452 | 552, None) => IsTooManyRecipients::Maybe,
216 (
217 _,
218 Some(EnhancedStatusCode {
227 class: 4,
228 subject: 5,
229 detail: 3,
230 }),
231 ) => IsTooManyRecipients::Yes,
232 _ => IsTooManyRecipients::No,
233 }
234 }
235
236 pub fn with_code_and_message(code: u16, message: &str) -> Self {
237 let lines: Vec<&str> = message.lines().collect();
238
239 let mut builder = ResponseBuilder::new(&ResponseLine {
240 code,
241 content: lines[0],
242 is_final: lines.len() == 1,
243 });
244
245 for (n, line) in lines.iter().enumerate().skip(1) {
246 builder
247 .add_line(&ResponseLine {
248 code,
249 content: line,
250 is_final: n == lines.len() - 1,
251 })
252 .ok();
253 }
254
255 builder.build(None)
256 }
257
258 pub fn was_due_to_message(&self) -> bool {
270 if let Some(command) = &self.command {
271 if let Ok(cmd) = Command::parse(command) {
272 return match cmd {
273 Command::MailFrom { .. }
274 | Command::RcptTo { .. }
275 | Command::Data
276 | Command::DataDot => true,
277 Command::Ehlo(_)
278 | Command::Helo(_)
279 | Command::Lhlo(_)
280 | Command::Rset
281 | Command::Quit
282 | Command::StartTls
283 | Command::Vrfy(_)
284 | Command::Expn(_)
285 | Command::Noop(_)
286 | Command::Help(_)
287 | Command::Auth { .. }
288 | Command::RawLine(_)
289 | Command::XClient(_) => false,
290 };
291 }
292 }
293
294 true
295 }
296}
297
298#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Hash)]
299pub struct EnhancedStatusCode {
300 pub class: u8,
301 pub subject: u16,
302 pub detail: u16,
303}
304
305fn parse_enhanced_status_code(line: &str) -> Option<(EnhancedStatusCode, &str)> {
306 let mut fields = line.splitn(3, '.');
307 let class = fields.next()?.parse::<u8>().ok()?;
308 if !matches!(class, 2 | 4 | 5) {
309 return None;
311 }
312 let subject = fields.next()?.parse::<u16>().ok()?;
313
314 let remainder = fields.next()?;
315 let mut fields = remainder.splitn(2, ' ');
316 let detail = fields.next()?.parse::<u16>().ok()?;
317 let remainder = fields.next()?;
318
319 Some((
320 EnhancedStatusCode {
321 class,
322 subject,
323 detail,
324 },
325 remainder,
326 ))
327}
328
329fn remove_line_break(data: &str) -> String {
330 let data = data.as_bytes();
331 let mut normalized = Vec::with_capacity(data.len());
332 let mut last_idx = 0;
333
334 for i in memchr::memchr2_iter(b'\r', b'\n', data) {
335 match data[i] {
336 b'\r' => {
337 normalized.extend_from_slice(&data[last_idx..i]);
338 if data.get(i + 1).copied() != Some(b'\n') {
339 normalized.push(b' ');
340 }
341 }
342 b'\n' => {
343 normalized.extend_from_slice(&data[last_idx..i]);
344 normalized.push(b' ');
345 }
346 _ => unreachable!(),
347 }
348 last_idx = i + 1;
349 }
350
351 normalized.extend_from_slice(&data[last_idx..]);
352 unsafe { String::from_utf8_unchecked(normalized) }
357}
358
359#[derive(Debug, PartialEq, Eq)]
360pub(crate) struct ResponseLine<'a> {
361 pub code: u16,
362 pub is_final: bool,
363 pub content: &'a str,
364}
365
366impl ResponseLine<'_> {
367 fn to_original_line(&self) -> String {
369 format!(
370 "{}{}{}",
371 self.code,
372 if self.is_final { " " } else { "-" },
373 self.content
374 )
375 }
376}
377
378pub(crate) struct ResponseBuilder {
379 pub code: u16,
380 pub enhanced_code: Option<EnhancedStatusCode>,
381 pub content: String,
382}
383
384impl ResponseBuilder {
385 pub fn new(parsed: &ResponseLine) -> Self {
386 let code = parsed.code;
387 let (enhanced_code, content) = match parse_enhanced_status_code(parsed.content) {
388 Some((enhanced, content)) => (Some(enhanced), content.to_string()),
389 None => (None, parsed.content.to_string()),
390 };
391
392 Self {
393 code,
394 enhanced_code,
395 content,
396 }
397 }
398
399 pub fn add_line(&mut self, parsed: &ResponseLine) -> Result<(), String> {
400 if parsed.code != self.code {
401 return Err(parsed.to_original_line());
402 }
403
404 self.content.push('\n');
405
406 let mut content = parsed.content;
407
408 if let Some(enh) = &self.enhanced_code {
409 let prefix = format!("{}.{}.{} ", enh.class, enh.subject, enh.detail);
410 if let Some(remainder) = parsed.content.strip_prefix(&prefix) {
411 content = remainder;
412 }
413 }
414
415 self.content.push_str(content);
416 Ok(())
417 }
418
419 pub fn build(self, command: Option<String>) -> Response {
420 Response {
421 code: self.code,
422 content: self.content,
423 enhanced_code: self.enhanced_code,
424 command,
425 }
426 }
427}
428
429#[allow(clippy::ptr_arg)]
430fn as_single_line<S>(content: &String, serializer: S) -> Result<S::Ok, S::Error>
431where
432 S: serde::Serializer,
433{
434 serializer.serialize_str(&remove_line_break(content))
435}
436
437#[cfg(test)]
438mod test {
439 use super::*;
440
441 #[test]
442 fn remove_crlf() {
443 fn remove(s: &str, expect: &str) {
444 assert_eq!(remove_line_break(s), expect, "input: {s:?}");
445 }
446
447 remove("hello\r\nthere\r\n", "hello there ");
448 remove("hello\r", "hello ");
449 remove("hello\nthere\r\n", "hello there ");
450 remove("hello\r\nthere\n", "hello there ");
451 remove("hello\r\r\r\nthere\n", "hello there ");
452 }
453
454 #[test]
455 fn response_parsing() {
456 assert_eq!(
457 parse_enhanced_status_code("2.0.1 w00t"),
458 Some((
459 EnhancedStatusCode {
460 class: 2,
461 subject: 0,
462 detail: 1
463 },
464 "w00t"
465 ))
466 );
467
468 assert_eq!(parse_enhanced_status_code("3.0.0 w00t"), None);
469
470 assert_eq!(parse_enhanced_status_code("2.0.0.1 w00t"), None);
471
472 assert_eq!(parse_enhanced_status_code("2.0.0.1w00t"), None);
473 }
474}