1use 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#[derive(PartialEq, Eq, Copy, Clone, Debug)]
22pub enum Severity {
23 PositiveCompletion = 2,
25 PositiveIntermediate = 3,
27 TransientNegativeCompletion = 4,
29 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#[derive(PartialEq, Eq, Copy, Clone, Debug)]
41pub enum Category {
42 Syntax = 0,
44 Information = 1,
46 Connections = 2,
48 Unspecified3 = 3,
50 Unspecified4 = 4,
52 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#[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#[derive(PartialEq, Eq, Copy, Clone, Debug)]
95pub struct Code {
96 pub severity: Severity,
98 pub category: Category,
100 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 pub fn new(severity: Severity, category: Category, detail: Detail) -> Code {
113 Code {
114 severity,
115 category,
116 detail,
117 }
118 }
119}
120
121#[derive(PartialEq, Eq, Clone, Debug)]
125pub struct Response {
126 pub code: Code,
128 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 pub fn new(code: Code, message: Vec<String>) -> Response {
144 Response { code, message }
145 }
146
147 pub fn is_positive(&self) -> bool {
149 matches!(
150 self.code.severity,
151 Severity::PositiveCompletion | Severity::PositiveIntermediate
152 )
153 }
154
155 pub fn has_code(&self, code: u16) -> bool {
157 self.code.to_string() == code.to_string()
158 }
159
160 pub fn first_word(&self) -> Option<&str> {
162 self.message
163 .first()
164 .and_then(|line| line.split_whitespace().next())
165 }
166
167 pub fn first_line(&self) -> Option<&str> {
169 self.message.first().map(String::as_str)
170 }
171}
172
173fn 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 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 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}