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