rfc5321/
client_types.rs

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    /// Compute theoretical maximum lifetime of a connection when
142    /// sending a single message
143    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    /// Compute theoretical maximum lifetime of a single message send
157    /// on an already established connection
158    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        // No other classes are defined
228        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    // This is safe because data comes from str, which is
271    // guaranteed to be valid utf8, and all we're manipulating
272    // above is whitespace which won't invalidate the utf8
273    // byte sequences in the data byte array
274    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    /// Reconsitute the original line that we parsed
286    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}