1use 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#[derive(PartialEq, Eq, Copy, Clone, Debug)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum Severity {
25 PositiveCompletion = 2,
27 PositiveIntermediate = 3,
29 TransientNegativeCompletion = 4,
31 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#[derive(PartialEq, Eq, Copy, Clone, Debug)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub enum Category {
45 Syntax = 0,
47 Information = 1,
49 Connections = 2,
51 Unspecified3 = 3,
53 Unspecified4 = 4,
55 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#[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#[derive(PartialEq, Eq, Copy, Clone, Debug)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100pub struct Code {
101 pub severity: Severity,
103 pub category: Category,
105 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 pub fn new(severity: Severity, category: Category, detail: Detail) -> Code {
118 Code {
119 severity,
120 category,
121 detail,
122 }
123 }
124
125 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#[derive(PartialEq, Eq, Clone, Debug)]
144#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
145pub struct Response {
146 code: Code,
148 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 pub fn new(code: Code, message: Vec<String>) -> Response {
166 Response { code, message }
167 }
168
169 pub fn is_positive(&self) -> bool {
171 self.code.is_positive()
172 }
173
174 pub fn has_code(&self, code: u16) -> bool {
176 self.code.to_string() == code.to_string()
177 }
178
179 pub fn first_word(&self) -> Option<&str> {
181 self.message
182 .first()
183 .and_then(|line| line.split_whitespace().next())
184 }
185
186 pub fn first_line(&self) -> Option<&str> {
188 self.message.first().map(String::as_str)
189 }
190
191 pub fn code(&self) -> Code {
193 self.code
194 }
195
196 pub fn message(&self) -> impl Iterator<Item = &str> {
198 self.message.iter().map(String::as_str)
199 }
200}
201
202fn 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 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 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}