rfc5321/
client_types.rs

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    /// Compute theoretical maximum lifetime of a connection when
143    /// sending a single message
144    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    /// Compute theoretical maximum lifetime of a single message send
158    /// on an already established connection
159    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        // RFC 5321 4.5.3.1.10: RFC 821 incorrectly ... "too many recipients" ..
196        // as having reply code 552 ... the correct reply ... is 452.
197        // Clients should treat 552 ... as a temporary
198        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    /// If the error contents were likely caused by something
224    /// about the mostly recently attempted message, rather than
225    /// a transport issue, or a carry-over from a prior message
226    /// (eg: previous message was rejected and destination chose
227    /// to drop the connection, which we detect later in RSET
228    /// on the next message), then we return true.
229    /// The expectation is that the caller will transiently
230    /// fail the message for later retry.
231    /// If we return false then the caller might decide to
232    /// try that same message again more immediately on
233    /// a separate connection
234    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        // No other classes are defined
274        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    // This is safe because data comes from str, which is
317    // guaranteed to be valid utf8, and all we're manipulating
318    // above is whitespace which won't invalidate the utf8
319    // byte sequences in the data byte array
320    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    /// Reconsitute the original line that we parsed
332    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}