1use super::Header;
8use crate::encoders::encode::rfc2047_encode_phrase;
9use std::borrow::Cow;
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
13pub struct EmailAddress<'x> {
14 pub name: Option<Cow<'x, str>>,
15 pub email: Cow<'x, str>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
20pub struct GroupedAddresses<'x> {
21 pub name: Option<Cow<'x, str>>,
22 pub addresses: Vec<Address<'x>>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
27pub enum Address<'x> {
28 Address(EmailAddress<'x>),
29 Group(GroupedAddresses<'x>),
30 List(Vec<Address<'x>>),
31}
32
33impl<'x> Address<'x> {
34 pub fn new_address(
36 name: Option<impl Into<Cow<'x, str>>>,
37 email: impl Into<Cow<'x, str>>,
38 ) -> Self {
39 Address::Address(EmailAddress {
40 name: name.map(|v| v.into()),
41 email: email.into(),
42 })
43 }
44
45 pub fn new_group(name: Option<impl Into<Cow<'x, str>>>, addresses: Vec<Address<'x>>) -> Self {
47 Address::Group(GroupedAddresses {
48 name: name.map(|v| v.into()),
49 addresses,
50 })
51 }
52
53 pub fn new_list(items: Vec<Address<'x>>) -> Self {
55 Address::List(items)
56 }
57
58 pub fn unwrap_address(&self) -> &EmailAddress<'x> {
59 match self {
60 Address::Address(address) => address,
61 _ => panic!("Address is not an EmailAddress"),
62 }
63 }
64}
65
66impl<'x> From<(&'x str, &'x str)> for Address<'x> {
67 fn from(value: (&'x str, &'x str)) -> Self {
68 Address::Address(EmailAddress {
69 name: Some(value.0.into()),
70 email: value.1.into(),
71 })
72 }
73}
74
75impl From<(String, String)> for Address<'_> {
76 fn from(value: (String, String)) -> Self {
77 Address::Address(EmailAddress {
78 name: Some(value.0.into()),
79 email: value.1.into(),
80 })
81 }
82}
83
84impl<'x> From<&'x str> for Address<'x> {
85 fn from(value: &'x str) -> Self {
86 Address::Address(EmailAddress {
87 name: None,
88 email: value.into(),
89 })
90 }
91}
92
93impl From<String> for Address<'_> {
94 fn from(value: String) -> Self {
95 Address::Address(EmailAddress {
96 name: None,
97 email: value.into(),
98 })
99 }
100}
101
102impl<'x, T> From<Vec<T>> for Address<'x>
103where
104 T: Into<Address<'x>>,
105{
106 fn from(value: Vec<T>) -> Self {
107 Address::new_list(value.into_iter().map(|x| x.into()).collect())
108 }
109}
110
111impl<'x, T, U> From<(U, Vec<T>)> for Address<'x>
112where
113 T: Into<Address<'x>>,
114 U: Into<Cow<'x, str>>,
115{
116 fn from(value: (U, Vec<T>)) -> Self {
117 Address::Group(GroupedAddresses {
118 name: Some(value.0.into()),
119 addresses: value.1.into_iter().map(|x| x.into()).collect(),
120 })
121 }
122}
123
124impl Header for Address<'_> {
125 fn write_header(
126 &self,
127 mut output: impl std::io::Write,
128 mut bytes_written: usize,
129 ) -> std::io::Result<usize> {
130 match self {
131 Address::Address(address) => {
132 address.write_header(&mut output, bytes_written)?;
133 }
134 Address::Group(group) => {
135 group.write_header(&mut output, bytes_written)?;
136 }
137 Address::List(list) => {
138 for (pos, address) in list.iter().enumerate() {
139 if bytes_written
140 + (match address {
141 Address::Address(address) => {
142 address.email.len()
143 + address.name.as_ref().map_or(0, |n| n.len() + 3)
144 + 2
145 }
146 Address::Group(group) => {
147 group.name.as_ref().map_or(0, |name| name.len() + 2)
148 }
149 Address::List(_) => 0,
150 })
151 >= 76
152 {
153 output.write_all(b"\r\n\t")?;
154 bytes_written = 1;
155 }
156
157 match address {
158 Address::Address(address) => {
159 bytes_written += address.write_header(&mut output, bytes_written)?;
160 if pos < list.len() - 1 {
161 output.write_all(b", ")?;
162 bytes_written += 1;
163 }
164 }
165 Address::Group(group) => {
166 bytes_written += group.write_header(&mut output, bytes_written)?;
167 if pos < list.len() - 1 {
168 output.write_all(b" ")?;
169 bytes_written += 1;
170 }
171 }
172 Address::List(_) => unreachable!(),
173 }
174 }
175 }
176 }
177 output.write_all(b"\r\n")?;
178 Ok(0)
179 }
180}
181
182impl Header for EmailAddress<'_> {
183 fn write_header(
184 &self,
185 mut output: impl std::io::Write,
186 mut bytes_written: usize,
187 ) -> std::io::Result<usize> {
188 if let Some(name) = &self.name {
189 bytes_written += rfc2047_encode_phrase(name, &mut output)?;
190 if bytes_written + self.email.len() + 2 >= 76 {
191 output.write_all(b"\r\n\t")?;
192 bytes_written = 1;
193 } else {
194 output.write_all(b" ")?;
195 bytes_written += 1;
196 }
197 }
198
199 output.write_all(b"<")?;
200 output.write_all(self.email.as_bytes())?;
201 output.write_all(b">")?;
202
203 Ok(bytes_written + self.email.len() + 2)
204 }
205}
206
207impl Header for GroupedAddresses<'_> {
208 fn write_header(
209 &self,
210 mut output: impl std::io::Write,
211 mut bytes_written: usize,
212 ) -> std::io::Result<usize> {
213 if let Some(name) = &self.name {
214 bytes_written += rfc2047_encode_phrase(name, &mut output)? + 2;
215 output.write_all(b": ")?;
216 }
217
218 for (pos, address) in self.addresses.iter().enumerate() {
219 let address = address.unwrap_address();
220
221 if bytes_written
222 + address.email.len()
223 + address.name.as_ref().map_or(0, |n| n.len() + 3)
224 + 2
225 >= 76
226 {
227 output.write_all(b"\r\n\t")?;
228 bytes_written = 1;
229 }
230
231 bytes_written += address.write_header(&mut output, bytes_written)?;
232 if pos < self.addresses.len() - 1 {
233 output.write_all(b", ")?;
234 bytes_written += 2;
235 }
236 }
237
238 output.write_all(b";")?;
239 bytes_written += 1;
240
241 Ok(bytes_written)
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use mail_parser::MessageParser;
249
250 fn build(address: Address<'_>) -> String {
251 let mut output = Vec::new();
252 address.write_header(&mut output, 4).unwrap();
253 String::from_utf8(output).unwrap()
254 }
255
256 fn parse(header: &str) -> Vec<(Option<String>, Option<String>)> {
257 let raw = format!("Cc: {header}\r\n");
258 let message = MessageParser::new().parse_headers(raw.as_bytes()).unwrap();
259 message
260 .cc()
261 .unwrap()
262 .iter()
263 .map(|addr| {
264 (
265 addr.name().map(str::to_string),
266 addr.address().map(str::to_string),
267 )
268 })
269 .collect()
270 }
271
272 #[test]
273 fn encoded_display_name_is_not_wrapped_in_quotes() {
274 let header = build(Address::new_address(
275 Some("Anna Müller"),
276 "anna@example.com",
277 ));
278 assert!(
279 !header.contains("\"=?"),
280 "quoted encoded-word in {header:?}"
281 );
282 assert!(header.contains("=?utf-8?"), "not encoded in {header:?}");
283 assert_eq!(
284 parse(&header),
285 vec![(
286 Some("Anna Müller".to_string()),
287 Some("anna@example.com".to_string())
288 )]
289 );
290 }
291
292 #[test]
293 fn base64_display_name_is_not_wrapped_in_quotes() {
294 let name = "Δοκιμή, Εταιρεία";
295 let header = build(Address::new_address(Some(name), "info@example.org"));
296 assert!(header.contains("=?utf-8?B?"), "not base64 in {header:?}");
297 assert!(
298 !header.contains("\"=?"),
299 "quoted encoded-word in {header:?}"
300 );
301 assert_eq!(
302 parse(&header),
303 vec![(Some(name.to_string()), Some("info@example.org".to_string()))]
304 );
305 }
306
307 #[test]
308 fn display_name_containing_quotes_and_comma_round_trips() {
309 let name = "\"Steuerberater, Wirtschaftsprüfer\"";
310 let header = build(Address::new_list(vec![
311 Address::new_address(Some("Anna Müller"), "anna@example.com"),
312 Address::new_address(Some(name), "kanzlei@example.org"),
313 ]));
314
315 assert!(
316 !header.contains("\"=?"),
317 "quoted encoded-word in {header:?}"
318 );
319
320 assert_eq!(
321 parse(&header),
322 vec![
323 (
324 Some("Anna Müller".to_string()),
325 Some("anna@example.com".to_string())
326 ),
327 (
328 Some(name.to_string()),
329 Some("kanzlei@example.org".to_string())
330 ),
331 ],
332 "{header:?}"
333 );
334 }
335
336 #[test]
337 fn comma_in_encoded_display_name_does_not_split_list() {
338 let header = build(Address::new_list(vec![
339 Address::new_address(Some("Müller, Anna"), "anna@example.com"),
340 Address::new_address(Some("Beispiel GmbH"), "info@example.org"),
341 ]));
342 assert_eq!(
343 header.matches(',').count(),
344 1,
345 "comma left unescaped inside encoded-word in {header:?}"
346 );
347
348 let parsed = parse(&header);
349 assert_eq!(parsed.len(), 2, "{header:?}");
350 assert_eq!(parsed[0].0.as_deref(), Some("Müller, Anna"), "{header:?}");
351 }
352
353 #[test]
354 fn phrase_encoded_word_escapes_specials() {
355 let header = build(Address::new_address(
356 Some("Meier (Kanzlei) <x>; [y] \"z\" @ w\\v: u, tü"),
357 "info@example.org",
358 ));
359 assert!(header.contains("?Q?"), "not Q encoded in {header:?}");
360
361 let encoded = header
362 .split_once("?Q?")
363 .unwrap()
364 .1
365 .split_once("?=")
366 .unwrap()
367 .0;
368
369 for ch in [
370 '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '"', '.',
371 ] {
372 assert!(!encoded.contains(ch), "raw {ch:?} in {encoded:?}");
373 }
374
375 assert_eq!(
376 parse(&header),
377 vec![(
378 Some("Meier (Kanzlei) <x>; [y] \"z\" @ w\\v: u, tü".to_string()),
379 Some("info@example.org".to_string())
380 )]
381 );
382 }
383
384 #[test]
385 fn ascii_display_name_with_comma_uses_quoted_string() {
386 let header = build(Address::new_address(Some("Doe, John"), "john@example.com"));
387 assert!(header.contains("\"Doe, John\""), "{header:?}");
388 assert_eq!(
389 parse(&header),
390 vec![(
391 Some("Doe, John".to_string()),
392 Some("john@example.com".to_string())
393 )]
394 );
395 }
396
397 #[test]
398 fn ascii_display_name_with_quotes_is_escaped() {
399 let name = "John \"JD\" Doe";
400 let header = build(Address::new_address(Some(name), "john@example.com"));
401 assert_eq!(
402 parse(&header),
403 vec![(Some(name.to_string()), Some("john@example.com".to_string()))],
404 "{header:?}"
405 );
406 }
407
408 #[test]
409 fn group_name_with_specials_round_trips() {
410 let header = build(Address::new_group(
411 Some("Büro, Empfang"),
412 vec![Address::new_address(
413 Some("Anna Müller"),
414 "anna@example.com",
415 )],
416 ));
417 assert!(
418 !header.contains("\"=?"),
419 "quoted encoded-word in {header:?}"
420 );
421
422 let raw = format!("Cc: {header}\r\n");
423 let message = MessageParser::new().parse_headers(raw.as_bytes()).unwrap();
424 let groups = message.cc().unwrap().as_group().expect("not a group");
425
426 assert_eq!(groups.len(), 1, "{header:?}");
427 assert_eq!(
428 groups[0].name.as_deref(),
429 Some("Büro, Empfang"),
430 "{header:?}"
431 );
432 assert_eq!(groups[0].addresses.len(), 1, "{header:?}");
433 assert_eq!(
434 groups[0].addresses[0].name.as_deref(),
435 Some("Anna Müller"),
436 "{header:?}"
437 );
438 }
439}