Skip to main content

mail_parser/parsers/fields/
address.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use crate::{Addr, Address, Group, HeaderValue, parsers::MessageStream};
8use std::borrow::Cow;
9
10#[derive(PartialEq, Clone, Copy, Debug)]
11enum AddressState {
12    Address,
13    Name,
14    Quote,
15    Comment,
16}
17
18pub struct AddressParser<'x> {
19    token_start: usize,
20    token_end: usize,
21
22    is_token_email: bool,
23    is_token_start: bool,
24    is_escaped: bool,
25
26    name_tokens: Vec<Cow<'x, str>>,
27    mail_tokens: Vec<Cow<'x, str>>,
28    comment_tokens: Vec<Cow<'x, str>>,
29
30    state: AddressState,
31    state_stack: Vec<AddressState>,
32
33    addresses: Vec<Addr<'x>>,
34    group_name: Option<Cow<'x, str>>,
35    group_comment: Option<Cow<'x, str>>,
36    result: Vec<Group<'x>>,
37}
38
39impl<'x> AddressParser<'x> {
40    pub fn add_token(&mut self, stream: &MessageStream<'x>, add_trail_space: bool) {
41        if self.token_start > 0 {
42            let token = String::from_utf8_lossy(&stream.data[self.token_start - 1..self.token_end]);
43            let mut add_space = false;
44            let list = match self.state {
45                AddressState::Address => &mut self.mail_tokens,
46                AddressState::Name => {
47                    if self.is_token_email {
48                        &mut self.mail_tokens
49                    } else {
50                        add_space = true;
51                        &mut self.name_tokens
52                    }
53                }
54                AddressState::Quote => &mut self.name_tokens,
55                AddressState::Comment => {
56                    add_space = true;
57                    &mut self.comment_tokens
58                }
59            };
60
61            if add_space && !list.is_empty() {
62                list.push(" ".into());
63            }
64
65            list.push(token);
66
67            if add_trail_space {
68                list.push(" ".into());
69            }
70
71            self.token_start = 0;
72            self.is_token_email = false;
73            self.is_token_start = true;
74            self.is_escaped = false;
75        }
76    }
77
78    pub fn add_address(&mut self) {
79        let has_mail = !self.mail_tokens.is_empty();
80        let has_name = !self.name_tokens.is_empty();
81        let has_comment = !self.comment_tokens.is_empty();
82
83        self.addresses.push(if has_mail && has_name && has_comment {
84            Addr {
85                name: Some(
86                    format!(
87                        "{} ({})",
88                        concat_tokens(&mut self.name_tokens),
89                        concat_tokens(&mut self.comment_tokens)
90                    )
91                    .into(),
92                ),
93                address: concat_tokens(&mut self.mail_tokens).into(),
94            }
95        } else if has_name && has_mail {
96            Addr {
97                name: concat_tokens(&mut self.name_tokens).into(),
98                address: concat_tokens(&mut self.mail_tokens).into(),
99            }
100        } else if has_mail && has_comment {
101            Addr {
102                name: concat_tokens(&mut self.comment_tokens).into(),
103                address: concat_tokens(&mut self.mail_tokens).into(),
104            }
105        } else if has_mail {
106            Addr {
107                name: None,
108                address: concat_tokens(&mut self.mail_tokens).into(),
109            }
110        } else if has_name && has_comment {
111            Addr {
112                name: concat_tokens(&mut self.comment_tokens).into(),
113                address: concat_tokens(&mut self.name_tokens).into(),
114            }
115        } else if has_name {
116            Addr {
117                name: concat_tokens(&mut self.name_tokens).into(),
118                address: None,
119            }
120        } else if has_comment {
121            Addr {
122                name: concat_tokens(&mut self.comment_tokens).into(),
123                address: None,
124            }
125        } else {
126            return;
127        });
128    }
129
130    pub fn add_group_details(&mut self) {
131        if !self.name_tokens.is_empty() {
132            self.group_name = concat_tokens(&mut self.name_tokens).into();
133        }
134
135        if !self.comment_tokens.is_empty() {
136            self.group_comment = concat_tokens(&mut self.comment_tokens).into();
137        }
138
139        if !self.mail_tokens.is_empty() {
140            if let Some(group_name) = &self.group_name {
141                self.group_name =
142                    Some(format!("{} {}", group_name, concat_tokens(&mut self.mail_tokens)).into());
143            } else {
144                self.group_name = concat_tokens(&mut self.mail_tokens).into();
145            }
146        }
147    }
148
149    pub fn add_group(&mut self) {
150        let has_name = self.group_name.is_some();
151        let has_comment = self.group_comment.is_some();
152        let has_addresses = !self.addresses.is_empty();
153
154        self.result
155            .push(if has_name && has_addresses && has_comment {
156                Group {
157                    name: Some(
158                        format!(
159                            "{} ({})",
160                            self.group_name.take().unwrap(),
161                            self.group_comment.take().unwrap()
162                        )
163                        .into(),
164                    ),
165                    addresses: std::mem::take(&mut self.addresses),
166                }
167            } else if has_addresses && has_name {
168                Group {
169                    name: self.group_name.take(),
170                    addresses: std::mem::take(&mut self.addresses),
171                }
172            } else if has_addresses {
173                Group {
174                    name: self.group_comment.take(),
175                    addresses: std::mem::take(&mut self.addresses),
176                }
177            } else if has_name {
178                Group {
179                    name: self.group_name.take(),
180                    addresses: Vec::new(),
181                }
182            } else {
183                return;
184            });
185    }
186}
187
188impl<'x> MessageStream<'x> {
189    pub fn parse_address(&mut self) -> HeaderValue<'x> {
190        let mut parser = AddressParser {
191            token_start: 0,
192            token_end: 0,
193
194            is_token_email: false,
195            is_token_start: true,
196            is_escaped: false,
197
198            name_tokens: Vec::with_capacity(3),
199            mail_tokens: Vec::with_capacity(3),
200            comment_tokens: Vec::with_capacity(3),
201
202            state: AddressState::Name,
203            state_stack: Vec::with_capacity(5),
204
205            addresses: Vec::new(),
206            group_name: None,
207            group_comment: None,
208            result: Vec::new(),
209        };
210
211        while let Some(ch) = self.next() {
212            match ch {
213                b'\n' => {
214                    parser.add_token(self, false);
215                    if parser.state == AddressState::Quote {
216                        if self.peek_next_is_space() {
217                            continue;
218                        } else {
219                            break;
220                        }
221                    }
222                    if self.try_next_is_space() {
223                        if !parser.is_token_start {
224                            parser.is_token_start = true;
225                        }
226                        continue;
227                    } else {
228                        break;
229                    }
230                }
231                b'\\' if parser.state != AddressState::Name && !parser.is_escaped => {
232                    if parser.token_start > 0 {
233                        if parser.state == AddressState::Quote {
234                            parser.token_end = self.offset() - 1;
235                        }
236                        parser.add_token(self, false);
237                    }
238                    parser.is_escaped = true;
239                    continue;
240                }
241                b',' if parser.state == AddressState::Name => {
242                    parser.add_token(self, false);
243                    parser.add_address();
244                    continue;
245                }
246                b'<' if parser.state == AddressState::Name => {
247                    parser.is_token_email = false;
248                    parser.add_token(self, false);
249                    parser.state_stack.push(AddressState::Name);
250                    parser.state = AddressState::Address;
251                    continue;
252                }
253                b'>' if parser.state == AddressState::Address => {
254                    parser.add_token(self, false);
255                    parser.state = parser.state_stack.pop().unwrap();
256                    continue;
257                }
258                b'"' if !parser.is_escaped => match parser.state {
259                    AddressState::Name => {
260                        parser.state_stack.push(AddressState::Name);
261                        parser.state = AddressState::Quote;
262                        parser.add_token(self, false);
263                        continue;
264                    }
265                    AddressState::Quote => {
266                        parser.add_token(self, false);
267                        parser.state = parser.state_stack.pop().unwrap();
268                        continue;
269                    }
270                    _ => (),
271                },
272                b'@' if parser.state == AddressState::Name => {
273                    parser.is_token_email = true;
274                }
275                b'=' if parser.is_token_start && !parser.is_escaped && self.peek_char(b'?') => {
276                    self.checkpoint();
277                    if let Some(token) = self.decode_rfc2047() {
278                        let add_space = parser.state != AddressState::Quote; // Make borrow-checker happy
279                        parser.add_token(self, add_space);
280                        (if parser.state != AddressState::Comment {
281                            &mut parser.name_tokens
282                        } else {
283                            &mut parser.comment_tokens
284                        })
285                        .push(token.into());
286                        continue;
287                    }
288                    self.restore();
289                }
290                b' ' | b'\t' => {
291                    if !parser.is_token_start {
292                        parser.is_token_start = true;
293                    }
294                    if parser.is_escaped {
295                        parser.is_escaped = false;
296                    }
297                    if parser.state == AddressState::Quote {
298                        if parser.token_start == 0 {
299                            parser.token_start = self.offset();
300                            parser.token_end = parser.token_start;
301                        } else {
302                            parser.token_end = self.offset();
303                        }
304                    }
305                    continue;
306                }
307                b'\r' => continue,
308                b'(' if parser.state != AddressState::Quote && !parser.is_escaped => {
309                    parser.state_stack.push(parser.state);
310                    if parser.state != AddressState::Comment {
311                        parser.add_token(self, false);
312                        parser.state = AddressState::Comment;
313                        continue;
314                    }
315                }
316                b')' if parser.state == AddressState::Comment && !parser.is_escaped => {
317                    let new_state = parser.state_stack.pop().unwrap();
318                    if parser.state != new_state {
319                        parser.add_token(self, false);
320                        parser.state = new_state;
321                        continue;
322                    }
323                }
324                b':' if parser.state == AddressState::Name && !parser.is_escaped => {
325                    parser.add_group();
326                    parser.add_token(self, false);
327                    parser.add_group_details();
328                    continue;
329                }
330                b';' if parser.state == AddressState::Name => {
331                    parser.add_token(self, false);
332                    parser.add_address();
333                    parser.add_group();
334                    continue;
335                }
336                _ => (),
337            }
338
339            if parser.is_escaped {
340                parser.is_escaped = false;
341            }
342
343            if parser.is_token_start {
344                parser.is_token_start = false;
345            }
346
347            if parser.token_start == 0 {
348                parser.token_start = self.offset();
349                parser.token_end = parser.token_start;
350            } else {
351                parser.token_end = self.offset();
352            }
353        }
354
355        parser.add_address();
356
357        if parser.group_name.is_some() || !parser.result.is_empty() {
358            parser.add_group();
359            HeaderValue::Address(Address::Group(parser.result))
360        } else if !parser.addresses.is_empty() {
361            HeaderValue::Address(Address::List(parser.addresses))
362        } else {
363            HeaderValue::Empty
364        }
365    }
366}
367
368fn concat_tokens<'x>(tokens: &mut Vec<Cow<'x, str>>) -> Cow<'x, str> {
369    if tokens.len() == 1 {
370        tokens.pop().unwrap()
371    } else {
372        let result = tokens.concat();
373        tokens.clear();
374        result.into()
375    }
376}
377
378pub fn parse_address_local_part(addr: &str) -> Option<&str> {
379    let addr = addr.as_bytes();
380    let mut iter = addr.iter().enumerate();
381    while let Some((pos, &ch)) = iter.next() {
382        if ch == b'@' {
383            return if pos > 0 && iter.next().is_some() {
384                std::str::from_utf8(addr.get(..pos)?).ok()
385            } else {
386                None
387            };
388        } else if !ch.is_ascii() {
389            return None;
390        }
391    }
392
393    None
394}
395
396pub fn parse_address_domain(addr: &str) -> Option<&str> {
397    let addr = addr.as_bytes();
398    for (pos, &ch) in addr.iter().enumerate() {
399        if ch == b'@' {
400            return if pos > 0 && pos + 1 < addr.len() {
401                std::str::from_utf8(addr.get(pos + 1..)?).ok()
402            } else {
403                None
404            };
405        } else if !ch.is_ascii() {
406            return None;
407        }
408    }
409
410    None
411}
412
413pub fn parse_address_user_part(addr: &str) -> Option<&str> {
414    let addr = addr.as_bytes();
415
416    let mut iter = addr.iter().enumerate();
417    while let Some((pos, &ch)) = iter.next() {
418        if ch == b'+' {
419            if pos > 0 {
420                while let Some((_, &ch)) = iter.next() {
421                    if ch == b'@' && iter.next().is_some() {
422                        return std::str::from_utf8(addr.get(..pos)?).ok();
423                    }
424                }
425            }
426            return None;
427        } else if ch == b'@' {
428            return if pos > 0 && iter.next().is_some() {
429                std::str::from_utf8(addr.get(..pos)?).ok()
430            } else {
431                None
432            };
433        } else if !ch.is_ascii() {
434            return None;
435        }
436    }
437
438    None
439}
440
441pub fn parse_address_detail_part(addr: &str) -> Option<&str> {
442    let addr = addr.as_bytes();
443    let mut plus_pos = usize::MAX;
444
445    let mut iter = addr.iter().enumerate();
446    while let Some((pos, &ch)) = iter.next() {
447        if ch == b'+' {
448            plus_pos = pos + 1;
449        } else if ch == b'@' {
450            if plus_pos != usize::MAX && iter.next().is_some() {
451                return std::str::from_utf8(addr.get(plus_pos..pos)?).ok();
452            } else {
453                return None;
454            }
455        } else if !ch.is_ascii() {
456            return None;
457        }
458    }
459
460    None
461}
462
463#[cfg(test)]
464mod tests {
465    use crate::parsers::{MessageStream, fields::load_tests};
466
467    #[test]
468    fn parse_addresses() {
469        for test in load_tests("address.json") {
470            assert_eq!(
471                MessageStream::new(test.header.as_bytes())
472                    .parse_address()
473                    .unwrap_address(),
474                test.expected,
475                "failed for {:?}",
476                test.header
477            );
478        }
479    }
480}