Skip to main content

io_smtp/rfc5321/types/
response.rs

1//! SMTP response (RFC 5321 ยง4.2).
2//!
3//! A complete reply, one code and one or more text lines, as parsed
4//! from the wire.
5
6use alloc::vec::Vec;
7
8use bounded_static_derive::ToStatic;
9use chumsky::prelude::*;
10
11use crate::rfc5321::types::{reply_code::SmtpReplyCode, text::SmtpText, vec1::SmtpVec1};
12
13/// A complete SMTP response (possibly multi-line).
14#[derive(Debug, Clone, PartialEq, Eq, Hash, ToStatic)]
15pub struct SmtpResponse<'a> {
16    /// The 3-digit reply code
17    pub code: SmtpReplyCode,
18    /// One or more response lines
19    pub lines: SmtpVec1<SmtpText<'a>>,
20}
21
22impl SmtpResponse<'_> {
23    /// Returns true if `buf` contains a complete SMTP response.
24    ///
25    /// A response is complete when the last CRLF-terminated line has
26    /// `ddd SP` (not `ddd -`).
27    pub fn is_complete(buf: &[u8]) -> bool {
28        if !buf.ends_with(b"\r\n") {
29            return false;
30        }
31
32        let body = &buf[..buf.len() - 2];
33        let line_start = body
34            .iter()
35            .rposition(|&b| b == b'\n')
36            .map(|p| p + 1)
37            .unwrap_or(0);
38
39        let last_line = &body[line_start..];
40        last_line.len() >= 4 && last_line[3] == b' '
41    }
42
43    /// Parses a response from raw bytes.
44    pub fn parse<'a>(buf: &'a [u8]) -> Result<SmtpResponse<'a>, Vec<Rich<'a, u8>>> {
45        parsers::response().parse(buf).into_result()
46    }
47
48    /// Creates a new single-line response.
49    pub fn new<'a>(code: SmtpReplyCode, text: SmtpText<'a>) -> SmtpResponse<'a> {
50        SmtpResponse {
51            code,
52            lines: SmtpVec1::from(text),
53        }
54    }
55
56    /// Creates a new multi-line response.
57    pub fn new_multiline<'a>(
58        code: SmtpReplyCode,
59        lines: SmtpVec1<SmtpText<'a>>,
60    ) -> SmtpResponse<'a> {
61        SmtpResponse { code, lines }
62    }
63
64    /// Returns true if this is a success response.
65    pub fn is_success(&self) -> bool {
66        self.code.is_success()
67    }
68
69    /// Returns true if this is an error response.
70    pub fn is_error(&self) -> bool {
71        self.code.is_error()
72    }
73
74    /// Returns the first (or only) line of text.
75    pub fn text(&self) -> &SmtpText<'_> {
76        &self.lines.as_ref()[0]
77    }
78}
79
80pub(crate) mod parsers {
81    //! Chumsky parser for the SMTP response.
82
83    use alloc::{borrow::Cow, vec::Vec};
84
85    use chumsky::prelude::*;
86
87    use crate::{
88        rfc5321::types::{
89            reply_code::parsers::reply_code as reply_code_parser,
90            response::SmtpResponse,
91            text::{SmtpText, parsers::text as text_parser},
92            vec1::SmtpVec1,
93        },
94        utils::parsers::{Extra, crlf, sp},
95    };
96
97    /// SMTP response parser.
98    ///
99    /// ```abnf
100    /// Replies        = *( Reply-line ) Final-Reply
101    /// Reply-line     = Reply-code "-" [ textstring ] CRLF
102    /// Final-Reply    = Reply-code SP [ textstring ] CRLF
103    /// Reply-code     = %x32-35 %x30-35 %x30-39
104    /// ```
105    pub(crate) fn response<'a>() -> impl Parser<'a, &'a [u8], SmtpResponse<'a>, Extra<'a>> + Clone {
106        // NOTE: continuation: code '-' [text] CRLF
107        let cont = reply_code_parser()
108            .then_ignore(just(b'-'))
109            .then(text_parser().or_not())
110            .then_ignore(crlf());
111        // NOTE: final: code SP [text] CRLF
112        let last = reply_code_parser()
113            .then_ignore(sp())
114            .then(text_parser().or_not())
115            .then_ignore(crlf());
116
117        cont.repeated()
118            .collect::<Vec<_>>()
119            .then(last)
120            .map(|(conts, (code, last_text))| {
121                let mut lines: Vec<SmtpText> = conts
122                    .into_iter()
123                    .map(|(_, t)| t.unwrap_or(SmtpText(Cow::Borrowed(""))))
124                    .collect();
125                lines.push(last_text.unwrap_or(SmtpText(Cow::Borrowed(""))));
126                let lines = SmtpVec1::unvalidated(lines);
127                SmtpResponse::new_multiline(code, lines)
128            })
129            .labelled("SMTP response")
130    }
131}