1use chrono::{DateTime, FixedOffset};
2use nom::{
3 branch::alt,
4 bytes::complete::{is_a, tag},
5 combinator::{map, not, opt},
6 multi::many0,
7 sequence::{terminated, tuple},
8 IResult,
9};
10
11use crate::imf::{datetime, mailbox};
12use crate::text::{ascii, misc_token, whitespace};
13
14#[derive(Debug, PartialEq)]
15pub enum ReceivedLogToken<'a> {
16 Addr(mailbox::AddrSpec<'a>),
17 Domain(mailbox::Domain<'a>),
18 Word(misc_token::Word<'a>),
19}
20
21#[derive(Debug, PartialEq)]
22pub struct ReceivedLog<'a> {
23 pub log: Vec<ReceivedLogToken<'a>>,
24 pub date: Option<DateTime<FixedOffset>>,
25}
26
27pub fn received_log(input: &[u8]) -> IResult<&[u8], ReceivedLog> {
39 map(
40 tuple((many0(received_tokens), tag(";"), datetime::section)),
41 |(tokens, _, dt)| ReceivedLog {
42 log: tokens,
43 date: dt,
44 },
45 )(input)
46}
47
48pub fn return_path(input: &[u8]) -> IResult<&[u8], Option<mailbox::AddrSpec>> {
49 alt((map(mailbox::angle_addr, Some), empty_path))(input)
50}
51
52fn empty_path(input: &[u8]) -> IResult<&[u8], Option<mailbox::AddrSpec>> {
53 let (input, _) = tuple((
54 opt(whitespace::cfws),
55 tag(&[ascii::LT]),
56 opt(whitespace::cfws),
57 tag(&[ascii::GT]),
58 opt(whitespace::cfws),
59 ))(input)?;
60 Ok((input, None))
61}
62
63fn received_tokens(input: &[u8]) -> IResult<&[u8], ReceivedLogToken> {
64 alt((
65 terminated(
66 map(misc_token::word, ReceivedLogToken::Word),
67 not(is_a([ascii::PERIOD, ascii::AT])),
68 ),
69 map(mailbox::angle_addr, ReceivedLogToken::Addr),
70 map(mailbox::addr_spec, ReceivedLogToken::Addr),
71 map(mailbox::obs_domain, ReceivedLogToken::Domain),
72 ))(input)
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::imf::trace::misc_token::Word;
79 use chrono::TimeZone;
80
81 #[test]
82 fn test_received_body() {
83 let hdrs = r#"from smtp.example.com ([10.83.2.2])
84 by server with LMTP
85 id xxxxxxxxx
86 (envelope-from <gitlab@example.com>)
87 for <me@example.com>; Tue, 13 Jun 2023 19:01:08 +0000"#
88 .as_bytes();
89
90 assert_eq!(
91 received_log(hdrs),
92 Ok((
93 &b""[..],
94 ReceivedLog {
95 date: Some(
96 FixedOffset::east_opt(0)
97 .unwrap()
98 .with_ymd_and_hms(2023, 06, 13, 19, 1, 8)
99 .unwrap()
100 ),
101 log: vec![
102 ReceivedLogToken::Word(Word::Atom(&b"from"[..])),
103 ReceivedLogToken::Domain(mailbox::Domain::Atoms(vec![
104 &b"smtp"[..],
105 &b"example"[..],
106 &b"com"[..]
107 ])),
108 ReceivedLogToken::Word(Word::Atom(&b"by"[..])),
109 ReceivedLogToken::Word(Word::Atom(&b"server"[..])),
110 ReceivedLogToken::Word(Word::Atom(&b"with"[..])),
111 ReceivedLogToken::Word(Word::Atom(&b"LMTP"[..])),
112 ReceivedLogToken::Word(Word::Atom(&b"id"[..])),
113 ReceivedLogToken::Word(Word::Atom(&b"xxxxxxxxx"[..])),
114 ReceivedLogToken::Word(Word::Atom(&b"for"[..])),
115 ReceivedLogToken::Addr(mailbox::AddrSpec {
116 local_part: mailbox::LocalPart(vec![mailbox::LocalPartToken::Word(
117 Word::Atom(&b"me"[..])
118 )]),
119 domain: mailbox::Domain::Atoms(vec![&b"example"[..], &b"com"[..]]),
120 })
121 ],
122 }
123 ))
124 );
125 }
126}