mail_parser/parsers/fields/
list.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 std::borrow::Cow;
8
9use crate::{parsers::MessageStream, HeaderValue};
10
11struct ListParser<'x> {
12    token_start: usize,
13    token_end: usize,
14    is_token_start: bool,
15    tokens: Vec<Cow<'x, str>>,
16    list: Vec<Cow<'x, str>>,
17}
18
19impl<'x> ListParser<'x> {
20    fn add_token(&mut self, stream: &MessageStream<'x>, add_space: bool) {
21        if self.token_start > 0 {
22            if !self.tokens.is_empty() {
23                self.tokens.push(" ".into());
24            }
25            self.tokens.push(String::from_utf8_lossy(
26                &stream.data[self.token_start - 1..self.token_end],
27            ));
28
29            if add_space {
30                self.tokens.push(" ".into());
31            }
32
33            self.token_start = 0;
34            self.is_token_start = true;
35        }
36    }
37
38    fn add_tokens_to_list(&mut self) {
39        if !self.tokens.is_empty() {
40            self.list.push(if self.tokens.len() == 1 {
41                self.tokens.pop().unwrap()
42            } else {
43                let value = self.tokens.concat();
44                self.tokens.clear();
45                value.into()
46            });
47        }
48    }
49}
50
51impl<'x> MessageStream<'x> {
52    pub fn parse_comma_separared(&mut self) -> HeaderValue<'x> {
53        let mut parser = ListParser {
54            token_start: 0,
55            token_end: 0,
56            is_token_start: true,
57            tokens: Vec::new(),
58            list: Vec::new(),
59        };
60
61        while let Some(ch) = self.next() {
62            match ch {
63                b'\n' => {
64                    parser.add_token(self, false);
65                    if !self.try_next_is_space() {
66                        parser.add_tokens_to_list();
67
68                        return match parser.list.len() {
69                            1 => HeaderValue::Text(parser.list.pop().unwrap()),
70                            0 => HeaderValue::Empty,
71                            _ => HeaderValue::TextList(parser.list),
72                        };
73                    } else {
74                        continue;
75                    }
76                }
77                b' ' | b'\t' => {
78                    if !parser.is_token_start {
79                        parser.is_token_start = true;
80                    }
81                    continue;
82                }
83                b'=' if parser.is_token_start && self.peek_char(b'?') => {
84                    self.checkpoint();
85                    if let Some(token) = self.decode_rfc2047() {
86                        parser.add_token(self, true);
87                        parser.tokens.push(token.into());
88                        continue;
89                    }
90                    self.restore();
91                }
92                b',' => {
93                    parser.add_token(self, false);
94                    parser.add_tokens_to_list();
95                    continue;
96                }
97                b'\r' => continue,
98                _ => (),
99            }
100
101            if parser.is_token_start {
102                parser.is_token_start = false;
103            }
104
105            if parser.token_start == 0 {
106                parser.token_start = self.offset();
107            }
108
109            parser.token_end = self.offset();
110        }
111
112        HeaderValue::Empty
113    }
114}
115#[cfg(test)]
116mod tests {
117    use crate::parsers::{fields::load_tests, MessageStream};
118
119    #[test]
120    fn parse_comma_separated_text() {
121        for test in load_tests::<Vec<String>>("list.json") {
122            assert_eq!(
123                MessageStream::new(test.header.as_bytes())
124                    .parse_comma_separared()
125                    .unwrap_text_list(),
126                test.expected,
127                "failed for {:?}",
128                test.header
129            );
130        }
131    }
132}