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::XClient(_) => false,
289 };
290 }
291 }
292
293 true
294 }
295}
296
297#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Hash)]
298pub struct EnhancedStatusCode {
299 pub class: u8,
300 pub subject: u16,
301 pub detail: u16,
302}
303
304fn parse_enhanced_status_code(line: &str) -> Option<(EnhancedStatusCode, &str)> {
305 let mut fields = line.splitn(3, '.');
306 let class = fields.next()?.parse::<u8>().ok()?;
307 if !matches!(class, 2 | 4 | 5) {
308 return None;
310 }
311 let subject = fields.next()?.parse::<u16>().ok()?;
312
313 let remainder = fields.next()?;
314 let mut fields = remainder.splitn(2, ' ');
315 let detail = fields.next()?.parse::<u16>().ok()?;
316 let remainder = fields.next()?;
317
318 Some((
319 EnhancedStatusCode {
320 class,
321 subject,
322 detail,
323 },
324 remainder,
325 ))
326}
327
328fn remove_line_break(data: &str) -> String {
329 let data = data.as_bytes();
330 let mut normalized = Vec::with_capacity(data.len());
331 let mut last_idx = 0;
332
333 for i in memchr::memchr2_iter(b'\r', b'\n', data) {
334 match data[i] {
335 b'\r' => {
336 normalized.extend_from_slice(&data[last_idx..i]);
337 if data.get(i + 1).copied() != Some(b'\n') {
338 normalized.push(b' ');
339 }
340 }
341 b'\n' => {
342 normalized.extend_from_slice(&data[last_idx..i]);
343 normalized.push(b' ');
344 }
345 _ => unreachable!(),
346 }
347 last_idx = i + 1;
348 }
349
350 normalized.extend_from_slice(&data[last_idx..]);
351 unsafe { String::from_utf8_unchecked(normalized) }
356}
357
358#[derive(Debug, PartialEq, Eq)]
359pub(crate) struct ResponseLine<'a> {
360 pub code: u16,
361 pub is_final: bool,
362 pub content: &'a str,
363}
364
365impl ResponseLine<'_> {
366 fn to_original_line(&self) -> String {
368 format!(
369 "{}{}{}",
370 self.code,
371 if self.is_final { " " } else { "-" },
372 self.content
373 )
374 }
375}
376
377pub(crate) struct ResponseBuilder {
378 pub code: u16,
379 pub enhanced_code: Option<EnhancedStatusCode>,
380 pub content: String,
381}
382
383impl ResponseBuilder {
384 pub fn new(parsed: &ResponseLine) -> Self {
385 let code = parsed.code;
386 let (enhanced_code, content) = match parse_enhanced_status_code(parsed.content) {
387 Some((enhanced, content)) => (Some(enhanced), content.to_string()),
388 None => (None, parsed.content.to_string()),
389 };
390
391 Self {
392 code,
393 enhanced_code,
394 content,
395 }
396 }
397
398 pub fn add_line(&mut self, parsed: &ResponseLine) -> Result<(), String> {
399 if parsed.code != self.code {
400 return Err(parsed.to_original_line());
401 }
402
403 self.content.push('\n');
404
405 let mut content = parsed.content;
406
407 if let Some(enh) = &self.enhanced_code {
408 let prefix = format!("{}.{}.{} ", enh.class, enh.subject, enh.detail);
409 if let Some(remainder) = parsed.content.strip_prefix(&prefix) {
410 content = remainder;
411 }
412 }
413
414 self.content.push_str(content);
415 Ok(())
416 }
417
418 pub fn build(self, command: Option<String>) -> Response {
419 Response {
420 code: self.code,
421 content: self.content,
422 enhanced_code: self.enhanced_code,
423 command,
424 }
425 }
426}
427
428#[allow(clippy::ptr_arg)]
429fn as_single_line<S>(content: &String, serializer: S) -> Result<S::Ok, S::Error>
430where
431 S: serde::Serializer,
432{
433 serializer.serialize_str(&remove_line_break(content))
434}
435
436#[cfg(test)]
437mod test {
438 use super::*;
439
440 #[test]
441 fn remove_crlf() {
442 fn remove(s: &str, expect: &str) {
443 assert_eq!(remove_line_break(s), expect, "input: {s:?}");
444 }
445
446 remove("hello\r\nthere\r\n", "hello there ");
447 remove("hello\r", "hello ");
448 remove("hello\nthere\r\n", "hello there ");
449 remove("hello\r\nthere\n", "hello there ");
450 remove("hello\r\r\r\nthere\n", "hello there ");
451 }
452
453 #[test]
454 fn response_parsing() {
455 assert_eq!(
456 parse_enhanced_status_code("2.0.1 w00t"),
457 Some((
458 EnhancedStatusCode {
459 class: 2,
460 subject: 0,
461 detail: 1
462 },
463 "w00t"
464 ))
465 );
466
467 assert_eq!(parse_enhanced_status_code("3.0.0 w00t"), None);
468
469 assert_eq!(parse_enhanced_status_code("2.0.0.1 w00t"), None);
470
471 assert_eq!(parse_enhanced_status_code("2.0.0.1w00t"), None);
472 }
473}