async_smtp/
response.rs

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