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