lettre/transport/smtp/
response.rs

1//! SMTP response, containing a mandatory return code and an optional text
2//! message
3
4use std::{
5    fmt::{Display, Formatter, Result},
6    result,
7    str::FromStr,
8};
9
10use nom::{
11    branch::alt,
12    bytes::streaming::{tag, take_until},
13    combinator::{complete, map},
14    multi::many0,
15    sequence::preceded,
16    IResult, Parser,
17};
18
19use crate::transport::smtp::{error, Error};
20
21/// The first digit indicates severity
22#[derive(PartialEq, Eq, Copy, Clone, Debug)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum Severity {
25    /// 2yx
26    PositiveCompletion = 2,
27    /// 3yz
28    PositiveIntermediate = 3,
29    /// 4yz
30    TransientNegativeCompletion = 4,
31    /// 5yz
32    PermanentNegativeCompletion = 5,
33}
34
35impl Display for Severity {
36    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
37        write!(f, "{}", *self as u8)
38    }
39}
40
41/// Second digit
42#[derive(PartialEq, Eq, Copy, Clone, Debug)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub enum Category {
45    /// x0z
46    Syntax = 0,
47    /// x1z
48    Information = 1,
49    /// x2z
50    Connections = 2,
51    /// x3z
52    Unspecified3 = 3,
53    /// x4z
54    Unspecified4 = 4,
55    /// x5z
56    MailSystem = 5,
57}
58
59impl Display for Category {
60    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
61        write!(f, "{}", *self as u8)
62    }
63}
64
65/// The detail digit of a response code (third digit)
66#[derive(PartialEq, Eq, Copy, Clone, Debug)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68pub enum Detail {
69    #[allow(missing_docs)]
70    Zero = 0,
71    #[allow(missing_docs)]
72    One = 1,
73    #[allow(missing_docs)]
74    Two = 2,
75    #[allow(missing_docs)]
76    Three = 3,
77    #[allow(missing_docs)]
78    Four = 4,
79    #[allow(missing_docs)]
80    Five = 5,
81    #[allow(missing_docs)]
82    Six = 6,
83    #[allow(missing_docs)]
84    Seven = 7,
85    #[allow(missing_docs)]
86    Eight = 8,
87    #[allow(missing_docs)]
88    Nine = 9,
89}
90
91impl Display for Detail {
92    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
93        write!(f, "{}", *self as u8)
94    }
95}
96
97/// Represents a 3 digit SMTP response code
98#[derive(PartialEq, Eq, Copy, Clone, Debug)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100pub struct Code {
101    /// First digit of the response code
102    pub severity: Severity,
103    /// Second digit of the response code
104    pub category: Category,
105    /// Third digit
106    pub detail: Detail,
107}
108
109impl Display for Code {
110    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
111        write!(f, "{}{}{}", self.severity, self.category, self.detail)
112    }
113}
114
115impl Code {
116    /// Creates a new `Code` structure
117    pub fn new(severity: Severity, category: Category, detail: Detail) -> Code {
118        Code {
119            severity,
120            category,
121            detail,
122        }
123    }
124
125    /// Tells if the response is positive
126    pub fn is_positive(self) -> bool {
127        matches!(
128            self.severity,
129            Severity::PositiveCompletion | Severity::PositiveIntermediate
130        )
131    }
132}
133
134impl From<Code> for u16 {
135    fn from(code: Code) -> Self {
136        code.detail as u16 + 10 * code.category as u16 + 100 * code.severity as u16
137    }
138}
139
140/// Contains an SMTP reply, with separated code and message
141///
142/// The text message is optional, only the code is mandatory
143#[derive(PartialEq, Eq, Clone, Debug)]
144#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
145pub struct Response {
146    /// Response code
147    code: Code,
148    /// Server response string (optional)
149    /// Handle multiline responses
150    message: Vec<String>,
151}
152
153impl FromStr for Response {
154    type Err = Error;
155
156    fn from_str(s: &str) -> result::Result<Response, Error> {
157        parse_response(s)
158            .map(|(_, r)| r)
159            .map_err(|e| error::response(e.to_owned()))
160    }
161}
162
163impl Response {
164    /// Creates a new `Response`
165    pub fn new(code: Code, message: Vec<String>) -> Response {
166        Response { code, message }
167    }
168
169    /// Tells if the response is positive
170    pub fn is_positive(&self) -> bool {
171        self.code.is_positive()
172    }
173
174    /// Tests code equality
175    pub fn has_code(&self, code: u16) -> bool {
176        self.code.to_string() == code.to_string()
177    }
178
179    /// Returns only the first word of the message if possible
180    pub fn first_word(&self) -> Option<&str> {
181        self.message
182            .first()
183            .and_then(|line| line.split_whitespace().next())
184    }
185
186    /// Returns only the line of the message if possible
187    pub fn first_line(&self) -> Option<&str> {
188        self.message.first().map(String::as_str)
189    }
190
191    /// Response code
192    pub fn code(&self) -> Code {
193        self.code
194    }
195
196    /// Server response string (array of lines)
197    pub fn message(&self) -> impl Iterator<Item = &str> {
198        self.message.iter().map(String::as_str)
199    }
200}
201
202// Parsers (originally from tokio-smtp)
203
204fn parse_code(i: &str) -> IResult<&str, Code> {
205    let (i, severity) = parse_severity(i)?;
206    let (i, category) = parse_category(i)?;
207    let (i, detail) = parse_detail(i)?;
208    Ok((
209        i,
210        Code {
211            severity,
212            category,
213            detail,
214        },
215    ))
216}
217
218fn parse_severity(i: &str) -> IResult<&str, Severity> {
219    alt((
220        map(tag("2"), |_| Severity::PositiveCompletion),
221        map(tag("3"), |_| Severity::PositiveIntermediate),
222        map(tag("4"), |_| Severity::TransientNegativeCompletion),
223        map(tag("5"), |_| Severity::PermanentNegativeCompletion),
224    ))
225    .parse(i)
226}
227
228fn parse_category(i: &str) -> IResult<&str, Category> {
229    alt((
230        map(tag("0"), |_| Category::Syntax),
231        map(tag("1"), |_| Category::Information),
232        map(tag("2"), |_| Category::Connections),
233        map(tag("3"), |_| Category::Unspecified3),
234        map(tag("4"), |_| Category::Unspecified4),
235        map(tag("5"), |_| Category::MailSystem),
236    ))
237    .parse(i)
238}
239
240fn parse_detail(i: &str) -> IResult<&str, Detail> {
241    alt((
242        map(tag("0"), |_| Detail::Zero),
243        map(tag("1"), |_| Detail::One),
244        map(tag("2"), |_| Detail::Two),
245        map(tag("3"), |_| Detail::Three),
246        map(tag("4"), |_| Detail::Four),
247        map(tag("5"), |_| Detail::Five),
248        map(tag("6"), |_| Detail::Six),
249        map(tag("7"), |_| Detail::Seven),
250        map(tag("8"), |_| Detail::Eight),
251        map(tag("9"), |_| Detail::Nine),
252    ))
253    .parse(i)
254}
255
256pub(crate) fn parse_response(i: &str) -> IResult<&str, Response> {
257    let (i, lines) = many0((
258        parse_code,
259        preceded(tag("-"), take_until("\r\n")),
260        tag("\r\n"),
261    ))
262    .parse(i)?;
263    let (i, (last_code, last_line)) =
264        (parse_code, preceded(tag(" "), take_until("\r\n"))).parse(i)?;
265    let (i, _) = complete(tag("\r\n")).parse(i)?;
266
267    // Check that all codes are equal.
268    if !lines.iter().all(|&(code, _, _)| code == last_code) {
269        return Err(nom::Err::Failure(nom::error::Error::new(
270            "",
271            nom::error::ErrorKind::Not,
272        )));
273    }
274
275    // Extract text from lines, and append last line.
276    let mut lines: Vec<String> = lines.into_iter().map(|(_, text, _)| text.into()).collect();
277    lines.push(last_line.into());
278
279    Ok((
280        i,
281        Response {
282            code: last_code,
283            message: lines,
284        },
285    ))
286}
287
288#[cfg(test)]
289mod test {
290    use super::*;
291
292    #[test]
293    fn test_severity_fmt() {
294        assert_eq!(format!("{}", Severity::PositiveCompletion), "2");
295    }
296
297    #[test]
298    fn test_category_fmt() {
299        assert_eq!(format!("{}", Category::Unspecified4), "4");
300    }
301
302    #[test]
303    fn test_code_new() {
304        assert_eq!(
305            Code::new(
306                Severity::TransientNegativeCompletion,
307                Category::Connections,
308                Detail::Zero,
309            ),
310            Code {
311                severity: Severity::TransientNegativeCompletion,
312                category: Category::Connections,
313                detail: Detail::Zero,
314            }
315        );
316    }
317
318    #[test]
319    fn test_code_display() {
320        let code = Code {
321            severity: Severity::TransientNegativeCompletion,
322            category: Category::Connections,
323            detail: Detail::One,
324        };
325
326        assert_eq!(code.to_string(), "421");
327    }
328
329    #[test]
330    fn test_code_to_u16() {
331        let code = Code {
332            severity: Severity::TransientNegativeCompletion,
333            category: Category::Connections,
334            detail: Detail::One,
335        };
336        let c: u16 = code.into();
337        assert_eq!(c, 421);
338    }
339
340    #[test]
341    fn test_response_from_str() {
342        let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
343        assert_eq!(
344            raw_response.parse::<Response>().unwrap(),
345            Response {
346                code: Code {
347                    severity: Severity::PositiveCompletion,
348                    category: Category::MailSystem,
349                    detail: Detail::Zero,
350                },
351                message: vec![
352                    "me".to_owned(),
353                    "8BITMIME".to_owned(),
354                    "SIZE 42".to_owned(),
355                    "AUTH PLAIN CRAM-MD5".to_owned(),
356                ],
357            }
358        );
359
360        let wrong_code = "2506-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
361        assert!(wrong_code.parse::<Response>().is_err());
362
363        let wrong_end = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250-AUTH PLAIN CRAM-MD5\r\n";
364        assert!(wrong_end.parse::<Response>().is_err());
365    }
366
367    #[test]
368    fn test_response_is_positive() {
369        assert!(Response::new(
370            Code {
371                severity: Severity::PositiveCompletion,
372                category: Category::MailSystem,
373                detail: Detail::Zero,
374            },
375            vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
376        )
377        .is_positive());
378        assert!(!Response::new(
379            Code {
380                severity: Severity::TransientNegativeCompletion,
381                category: Category::MailSystem,
382                detail: Detail::Zero,
383            },
384            vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
385        )
386        .is_positive());
387    }
388
389    #[test]
390    fn test_response_has_code() {
391        assert!(Response::new(
392            Code {
393                severity: Severity::TransientNegativeCompletion,
394                category: Category::MailSystem,
395                detail: Detail::One,
396            },
397            vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
398        )
399        .has_code(451));
400        assert!(!Response::new(
401            Code {
402                severity: Severity::TransientNegativeCompletion,
403                category: Category::MailSystem,
404                detail: Detail::One,
405            },
406            vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
407        )
408        .has_code(251));
409    }
410
411    #[test]
412    fn test_response_first_word() {
413        assert_eq!(
414            Response::new(
415                Code {
416                    severity: Severity::TransientNegativeCompletion,
417                    category: Category::MailSystem,
418                    detail: Detail::One,
419                },
420                vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
421            )
422            .first_word(),
423            Some("me")
424        );
425        assert_eq!(
426            Response::new(
427                Code {
428                    severity: Severity::TransientNegativeCompletion,
429                    category: Category::MailSystem,
430                    detail: Detail::One,
431                },
432                vec![
433                    "me mo".to_owned(),
434                    "8BITMIME".to_owned(),
435                    "SIZE 42".to_owned(),
436                ],
437            )
438            .first_word(),
439            Some("me")
440        );
441        assert_eq!(
442            Response::new(
443                Code {
444                    severity: Severity::TransientNegativeCompletion,
445                    category: Category::MailSystem,
446                    detail: Detail::One,
447                },
448                vec![],
449            )
450            .first_word(),
451            None
452        );
453        assert_eq!(
454            Response::new(
455                Code {
456                    severity: Severity::TransientNegativeCompletion,
457                    category: Category::MailSystem,
458                    detail: Detail::One,
459                },
460                vec![" ".to_owned()],
461            )
462            .first_word(),
463            None
464        );
465        assert_eq!(
466            Response::new(
467                Code {
468                    severity: Severity::TransientNegativeCompletion,
469                    category: Category::MailSystem,
470                    detail: Detail::One,
471                },
472                vec!["  ".to_owned()],
473            )
474            .first_word(),
475            None
476        );
477        assert_eq!(
478            Response::new(
479                Code {
480                    severity: Severity::TransientNegativeCompletion,
481                    category: Category::MailSystem,
482                    detail: Detail::One,
483                },
484                vec!["".to_owned()],
485            )
486            .first_word(),
487            None
488        );
489    }
490
491    #[test]
492    fn test_response_incomplete() {
493        let raw_response = "250-smtp.example.org\r\n";
494        let res = parse_response(raw_response);
495        match res {
496            Err(nom::Err::Incomplete(_)) => {}
497            _ => panic!("Expected incomplete response, got {res:?}"),
498        }
499    }
500
501    #[test]
502    fn test_response_first_line() {
503        assert_eq!(
504            Response::new(
505                Code {
506                    severity: Severity::TransientNegativeCompletion,
507                    category: Category::MailSystem,
508                    detail: Detail::One,
509                },
510                vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
511            )
512            .first_line(),
513            Some("me")
514        );
515        assert_eq!(
516            Response::new(
517                Code {
518                    severity: Severity::TransientNegativeCompletion,
519                    category: Category::MailSystem,
520                    detail: Detail::One,
521                },
522                vec![
523                    "me mo".to_owned(),
524                    "8BITMIME".to_owned(),
525                    "SIZE 42".to_owned(),
526                ],
527            )
528            .first_line(),
529            Some("me mo")
530        );
531        assert_eq!(
532            Response::new(
533                Code {
534                    severity: Severity::TransientNegativeCompletion,
535                    category: Category::MailSystem,
536                    detail: Detail::One,
537                },
538                vec![],
539            )
540            .first_line(),
541            None
542        );
543        assert_eq!(
544            Response::new(
545                Code {
546                    severity: Severity::TransientNegativeCompletion,
547                    category: Category::MailSystem,
548                    detail: Detail::One,
549                },
550                vec![" ".to_owned()],
551            )
552            .first_line(),
553            Some(" ")
554        );
555        assert_eq!(
556            Response::new(
557                Code {
558                    severity: Severity::TransientNegativeCompletion,
559                    category: Category::MailSystem,
560                    detail: Detail::One,
561                },
562                vec!["  ".to_owned()],
563            )
564            .first_line(),
565            Some("  ")
566        );
567        assert_eq!(
568            Response::new(
569                Code {
570                    severity: Severity::TransientNegativeCompletion,
571                    category: Category::MailSystem,
572                    detail: Detail::One,
573                },
574                vec!["".to_owned()],
575            )
576            .first_line(),
577            Some("")
578        );
579    }
580}