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 is_too_many_recipients(&self) -> bool {
195 self.code == 452 || self.code == 552
199 }
200
201 pub fn with_code_and_message(code: u16, message: &str) -> Self {
202 let lines: Vec<&str> = message.lines().collect();
203
204 let mut builder = ResponseBuilder::new(&ResponseLine {
205 code,
206 content: lines[0],
207 is_final: lines.len() == 1,
208 });
209
210 for (n, line) in lines.iter().enumerate().skip(1) {
211 builder
212 .add_line(&ResponseLine {
213 code,
214 content: line,
215 is_final: n == lines.len() - 1,
216 })
217 .ok();
218 }
219
220 builder.build(None)
221 }
222
223 pub fn was_due_to_message(&self) -> bool {
235 if let Some(command) = &self.command {
236 if let Ok(cmd) = Command::parse(command) {
237 return match cmd {
238 Command::MailFrom { .. }
239 | Command::RcptTo { .. }
240 | Command::Data
241 | Command::DataDot => true,
242 Command::Ehlo(_)
243 | Command::Helo(_)
244 | Command::Lhlo(_)
245 | Command::Rset
246 | Command::Quit
247 | Command::StartTls
248 | Command::Vrfy(_)
249 | Command::Expn(_)
250 | Command::Noop(_)
251 | Command::Help(_)
252 | Command::Auth { .. }
253 | Command::XClient(_) => false,
254 };
255 }
256 }
257
258 true
259 }
260}
261
262#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Hash)]
263pub struct EnhancedStatusCode {
264 pub class: u8,
265 pub subject: u16,
266 pub detail: u16,
267}
268
269fn parse_enhanced_status_code(line: &str) -> Option<(EnhancedStatusCode, &str)> {
270 let mut fields = line.splitn(3, '.');
271 let class = fields.next()?.parse::<u8>().ok()?;
272 if !matches!(class, 2 | 4 | 5) {
273 return None;
275 }
276 let subject = fields.next()?.parse::<u16>().ok()?;
277
278 let remainder = fields.next()?;
279 let mut fields = remainder.splitn(2, ' ');
280 let detail = fields.next()?.parse::<u16>().ok()?;
281 let remainder = fields.next()?;
282
283 Some((
284 EnhancedStatusCode {
285 class,
286 subject,
287 detail,
288 },
289 remainder,
290 ))
291}
292
293fn remove_line_break(data: &str) -> String {
294 let data = data.as_bytes();
295 let mut normalized = Vec::with_capacity(data.len());
296 let mut last_idx = 0;
297
298 for i in memchr::memchr2_iter(b'\r', b'\n', data) {
299 match data[i] {
300 b'\r' => {
301 normalized.extend_from_slice(&data[last_idx..i]);
302 if data.get(i + 1).copied() != Some(b'\n') {
303 normalized.push(b' ');
304 }
305 }
306 b'\n' => {
307 normalized.extend_from_slice(&data[last_idx..i]);
308 normalized.push(b' ');
309 }
310 _ => unreachable!(),
311 }
312 last_idx = i + 1;
313 }
314
315 normalized.extend_from_slice(&data[last_idx..]);
316 unsafe { String::from_utf8_unchecked(normalized) }
321}
322
323#[derive(Debug, PartialEq, Eq)]
324pub(crate) struct ResponseLine<'a> {
325 pub code: u16,
326 pub is_final: bool,
327 pub content: &'a str,
328}
329
330impl ResponseLine<'_> {
331 fn to_original_line(&self) -> String {
333 format!(
334 "{}{}{}",
335 self.code,
336 if self.is_final { " " } else { "-" },
337 self.content
338 )
339 }
340}
341
342pub(crate) struct ResponseBuilder {
343 pub code: u16,
344 pub enhanced_code: Option<EnhancedStatusCode>,
345 pub content: String,
346}
347
348impl ResponseBuilder {
349 pub fn new(parsed: &ResponseLine) -> Self {
350 let code = parsed.code;
351 let (enhanced_code, content) = match parse_enhanced_status_code(parsed.content) {
352 Some((enhanced, content)) => (Some(enhanced), content.to_string()),
353 None => (None, parsed.content.to_string()),
354 };
355
356 Self {
357 code,
358 enhanced_code,
359 content,
360 }
361 }
362
363 pub fn add_line(&mut self, parsed: &ResponseLine) -> Result<(), String> {
364 if parsed.code != self.code {
365 return Err(parsed.to_original_line());
366 }
367
368 self.content.push('\n');
369
370 let mut content = parsed.content;
371
372 if let Some(enh) = &self.enhanced_code {
373 let prefix = format!("{}.{}.{} ", enh.class, enh.subject, enh.detail);
374 if let Some(remainder) = parsed.content.strip_prefix(&prefix) {
375 content = remainder;
376 }
377 }
378
379 self.content.push_str(content);
380 Ok(())
381 }
382
383 pub fn build(self, command: Option<String>) -> Response {
384 Response {
385 code: self.code,
386 content: self.content,
387 enhanced_code: self.enhanced_code,
388 command,
389 }
390 }
391}
392
393#[allow(clippy::ptr_arg)]
394fn as_single_line<S>(content: &String, serializer: S) -> Result<S::Ok, S::Error>
395where
396 S: serde::Serializer,
397{
398 serializer.serialize_str(&remove_line_break(content))
399}
400
401#[cfg(test)]
402mod test {
403 use super::*;
404
405 #[test]
406 fn remove_crlf() {
407 fn remove(s: &str, expect: &str) {
408 assert_eq!(remove_line_break(s), expect, "input: {s:?}");
409 }
410
411 remove("hello\r\nthere\r\n", "hello there ");
412 remove("hello\r", "hello ");
413 remove("hello\nthere\r\n", "hello there ");
414 remove("hello\r\nthere\n", "hello there ");
415 remove("hello\r\r\r\nthere\n", "hello there ");
416 }
417
418 #[test]
419 fn response_parsing() {
420 assert_eq!(
421 parse_enhanced_status_code("2.0.1 w00t"),
422 Some((
423 EnhancedStatusCode {
424 class: 2,
425 subject: 0,
426 detail: 1
427 },
428 "w00t"
429 ))
430 );
431
432 assert_eq!(parse_enhanced_status_code("3.0.0 w00t"), None);
433
434 assert_eq!(parse_enhanced_status_code("2.0.0.1 w00t"), None);
435
436 assert_eq!(parse_enhanced_status_code("2.0.0.1w00t"), None);
437 }
438}