1use nom::{
2 branch::alt,
3 bytes::complete::tag,
4 combinator::{into, map, opt},
5 multi::separated_list1,
6 sequence::tuple,
7 IResult,
8};
9
10use crate::imf::mailbox::{mailbox, MailboxRef};
12use crate::text::misc_token::{phrase, Phrase};
13use crate::text::whitespace::cfws;
14
15#[derive(Debug, PartialEq)]
16pub struct GroupRef<'a> {
17 pub name: Phrase<'a>,
18 pub participants: Vec<MailboxRef<'a>>,
19}
20
21#[derive(Debug, PartialEq)]
22pub enum AddressRef<'a> {
23 Single(MailboxRef<'a>),
24 Many(GroupRef<'a>),
25}
26impl<'a> From<MailboxRef<'a>> for AddressRef<'a> {
27 fn from(mx: MailboxRef<'a>) -> Self {
28 AddressRef::Single(mx)
29 }
30}
31impl<'a> From<GroupRef<'a>> for AddressRef<'a> {
32 fn from(grp: GroupRef<'a>) -> Self {
33 AddressRef::Many(grp)
34 }
35}
36pub type AddressList<'a> = Vec<AddressRef<'a>>;
37
38pub fn address(input: &[u8]) -> IResult<&[u8], AddressRef> {
44 alt((into(mailbox), into(group)))(input)
45}
46
47pub fn group(input: &[u8]) -> IResult<&[u8], GroupRef> {
54 let (input, (grp_name, _, grp_list, _, _)) =
55 tuple((phrase, tag(":"), opt(group_list), tag(";"), opt(cfws)))(input)?;
56
57 Ok((
58 input,
59 GroupRef {
60 name: grp_name,
61 participants: grp_list.unwrap_or(vec![]),
62 },
63 ))
64}
65
66pub fn group_list(input: &[u8]) -> IResult<&[u8], Vec<MailboxRef>> {
72 alt((mailbox_list, mailbox_cfws))(input)
73}
74
75fn mailbox_cfws(input: &[u8]) -> IResult<&[u8], Vec<MailboxRef>> {
76 let (input, _) = cfws(input)?;
77 Ok((input, vec![]))
78}
79
80pub fn mailbox_list(input: &[u8]) -> IResult<&[u8], Vec<MailboxRef>> {
86 separated_list1(tag(","), mailbox)(input)
87}
88
89pub fn address_list(input: &[u8]) -> IResult<&[u8], Vec<AddressRef>> {
95 separated_list1(tag(","), address)(input)
96}
97
98pub fn address_list_cfws(input: &[u8]) -> IResult<&[u8], Vec<AddressRef>> {
99 let (input, _) = cfws(input)?;
100 Ok((input, vec![]))
101}
102
103pub fn nullable_address_list(input: &[u8]) -> IResult<&[u8], Vec<AddressRef>> {
104 map(opt(alt((address_list, address_list_cfws))), |v| {
105 v.unwrap_or(vec![])
106 })(input)
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::imf::mailbox::{AddrSpec, Domain, LocalPart, LocalPartToken};
113 use crate::text::misc_token::{Phrase, Word};
114
115 #[test]
116 fn test_mailbox_list() {
117 match mailbox_list(
118 r#"Pete(A nice \) chap) <pete(his account)@silly.test(his host)>"#.as_bytes(),
119 ) {
120 Ok((rest, _)) => assert_eq!(&b""[..], rest),
121 _ => panic!(),
122 };
123
124 match mailbox_list(
125 r#"Mary Smith <mary@x.test>, jdoe@example.org, Who? <one@y.test>, <boss@nil.test>, "Giant; \"Big\" Box" <sysservices@example.net>"#.as_bytes(),
126 ) {
127 Ok((rest, _)) => assert_eq!(&b""[..], rest),
128 _ => panic!(),
129 };
130 }
131
132 #[test]
133 fn test_address_list() {
134 assert_eq!(
135 address_list(
136 r#"A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;, Mary Smith <mary@x.test>"#.as_bytes()
137 ),
138 Ok((
139 &b""[..],
140 vec![
141 AddressRef::Many(GroupRef {
142 name: Phrase(vec![Word::Atom(&b"A"[..]), Word::Atom(&b"Group"[..])]),
143 participants: vec![
144 MailboxRef {
145 name: Some(Phrase(vec![Word::Atom(&b"Ed"[..]), Word::Atom(&b"Jones"[..])])),
146 addrspec: AddrSpec {
147 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(&b"c"[..]))]),
148 domain: Domain::Atoms(vec![&b"a"[..], &b"test"[..]]),
149 },
150 },
151 MailboxRef {
152 name: None,
153 addrspec: AddrSpec {
154 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(&b"joe"[..]))]),
155 domain: Domain::Atoms(vec![&b"where"[..], &b"test"[..]])
156 },
157 },
158 MailboxRef {
159 name: Some(Phrase(vec![Word::Atom(&b"John"[..])])),
160 addrspec: AddrSpec {
161 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(&b"jdoe"[..]))]),
162 domain: Domain::Atoms(vec![&b"one"[..], &b"test"[..]])
163 },
164 },
165 ],
166 }),
167 AddressRef::Single(MailboxRef {
168 name: Some(Phrase(vec![Word::Atom(&b"Mary"[..]), Word::Atom(&b"Smith"[..])])),
169 addrspec: AddrSpec {
170 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(&b"mary"[..]))]),
171 domain: Domain::Atoms(vec![&b"x"[..], &b"test"[..]])
172 },
173 }),
174 ]
175 ))
176 );
177 }
178
179 use crate::text::encoding::{EncodedWord, QuotedChunk, QuotedWord};
180 use crate::text::quoted::QuotedString;
181
182 #[test]
183 fn test_strange_groups() {
184 assert_eq!(
185 address_list(
186 br#""Colleagues": "James Smythe" <james@vandelay.com>;, Friends:
187 jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= <john@example.com>;"#
188 ),
189 Ok((
190 &b""[..],
191 vec![
192 AddressRef::Many(GroupRef {
193 name: Phrase(vec![Word::Quoted(QuotedString(vec![&b"Colleagues"[..]]))]),
194 participants: vec![MailboxRef {
195 name: Some(Phrase(vec![Word::Quoted(QuotedString(vec![
196 &b"James"[..],
197 &b" "[..],
198 &b"Smythe"[..]
199 ]))])),
200 addrspec: AddrSpec {
201 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(
202 &b"james"[..]
203 ))]),
204 domain: Domain::Atoms(vec![&b"vandelay"[..], &b"com"[..]]),
205 }
206 },],
207 }),
208 AddressRef::Many(GroupRef {
209 name: Phrase(vec![Word::Atom(&b"Friends"[..])]),
210 participants: vec![
211 MailboxRef {
212 name: None,
213 addrspec: AddrSpec {
214 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(
215 &b"jane"[..]
216 ))]),
217 domain: Domain::Atoms(vec![&b"example"[..], &b"com"[..]]),
218 }
219 },
220 MailboxRef {
221 name: Some(Phrase(vec![Word::Encoded(EncodedWord::Quoted(
222 QuotedWord {
223 enc: encoding_rs::UTF_8,
224 chunks: vec![
225 QuotedChunk::Safe(&b"John"[..]),
226 QuotedChunk::Space,
227 QuotedChunk::Safe(&b"Sm"[..]),
228 QuotedChunk::Encoded(vec![0xc3, 0xae]),
229 QuotedChunk::Safe(&b"th"[..]),
230 ]
231 }
232 ))])),
233 addrspec: AddrSpec {
234 local_part: LocalPart(vec![LocalPartToken::Word(Word::Atom(
235 &b"john"[..]
236 ))]),
237 domain: Domain::Atoms(vec![&b"example"[..], &b"com"[..]]),
238 }
239 },
240 ]
241 }),
242 ]
243 ))
244 );
245 }
246}