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 (for RCPT TO)
198        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    /// If the error contents were likely caused by something
230    /// about the mostly recently attempted message, rather than
231    /// a transport issue, or a carry-over from a prior message
232    /// (eg: previous message was rejected and destination chose
233    /// to drop the connection, which we detect later in RSET
234    /// on the next message), then we return true.
235    /// The expectation is that the caller will transiently
236    /// fail the message for later retry.
237    /// If we return false then the caller might decide to
238    /// try that same message again more immediately on
239    /// a separate connection
240    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        // No other classes are defined
280        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    // This is safe because data comes from str, which is
323    // guaranteed to be valid utf8, and all we're manipulating
324    // above is whitespace which won't invalidate the utf8
325    // byte sequences in the data byte array
326    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    /// Reconsitute the original line that we parsed
338    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}