mailin/
parser.rs

1use nom::branch::alt;
2use nom::bytes::complete::{is_not, tag, tag_no_case, take_while1};
3use nom::character::is_alphanumeric;
4use nom::combinator::{map, map_res, value};
5use nom::sequence::{pair, preceded, separated_pair, terminated};
6use nom::IResult;
7
8use crate::response::*;
9use crate::smtp::{Cmd, Credentials};
10use std::str;
11
12//----- Parser -----------------------------------------------------------------
13
14// Parse a line from the client
15pub fn parse(line: &[u8]) -> Result<Cmd, Response> {
16    command(line).map(|r| r.1).map_err(|e| match e {
17        nom::Err::Incomplete(_) => MISSING_PARAMETER,
18        nom::Err::Error(_) => SYNTAX_ERROR,
19        nom::Err::Failure(_) => SYNTAX_ERROR,
20    })
21}
22
23// Parse an authentication response from the client
24pub fn parse_auth_response(line: &[u8]) -> Result<&[u8], Response> {
25    auth_response(line).map(|r| r.1).map_err(|_| SYNTAX_ERROR)
26}
27
28fn command(buf: &[u8]) -> IResult<&[u8], Cmd> {
29    terminated(
30        alt((
31            helo, ehlo, mail, rcpt, data, rset, quit, vrfy, noop, starttls, auth,
32        )),
33        tag(b"\r\n"),
34    )(buf)
35}
36
37fn hello_domain(buf: &[u8]) -> IResult<&[u8], &str> {
38    map_res(is_not(b" \t\r\n" as &[u8]), str::from_utf8)(buf)
39}
40
41fn helo(buf: &[u8]) -> IResult<&[u8], Cmd> {
42    let parse_domain = preceded(cmd(b"helo"), hello_domain);
43    map(parse_domain, |domain| Cmd::Helo { domain })(buf)
44}
45
46fn ehlo(buf: &[u8]) -> IResult<&[u8], Cmd> {
47    let parse_domain = preceded(cmd(b"ehlo"), hello_domain);
48    map(parse_domain, |domain| Cmd::Ehlo { domain })(buf)
49}
50
51fn mail_path(buf: &[u8]) -> IResult<&[u8], &str> {
52    map_res(is_not(b" <>\t\r\n" as &[u8]), str::from_utf8)(buf)
53}
54
55fn take_all(buf: &[u8]) -> IResult<&[u8], &str> {
56    map_res(is_not(b"\r\n" as &[u8]), str::from_utf8)(buf)
57}
58
59fn body_eq_8bit(buf: &[u8]) -> IResult<&[u8], bool> {
60    let preamble = pair(space, tag_no_case(b"body="));
61    let is8bit = alt((
62        value(true, tag_no_case(b"8bitmime")),
63        value(false, tag_no_case(b"7bit")),
64    ));
65    preceded(preamble, is8bit)(buf)
66}
67
68fn is8bitmime(buf: &[u8]) -> IResult<&[u8], bool> {
69    body_eq_8bit(buf).or(Ok((buf, false)))
70}
71
72fn mail(buf: &[u8]) -> IResult<&[u8], Cmd> {
73    let preamble = pair(cmd(b"mail"), tag_no_case(b"from:<"));
74    let mail_path_parser = preceded(preamble, mail_path);
75    let parser = separated_pair(mail_path_parser, tag(b">"), is8bitmime);
76    map(parser, |r| Cmd::Mail {
77        reverse_path: r.0,
78        is8bit: r.1,
79    })(buf)
80}
81
82fn rcpt(buf: &[u8]) -> IResult<&[u8], Cmd> {
83    let preamble = pair(cmd(b"rcpt"), tag_no_case(b"to:<"));
84    let mail_path_parser = preceded(preamble, mail_path);
85    let parser = terminated(mail_path_parser, tag(b">"));
86    map(parser, |path| Cmd::Rcpt { forward_path: path })(buf)
87}
88
89fn data(buf: &[u8]) -> IResult<&[u8], Cmd> {
90    value(Cmd::Data, tag_no_case(b"data"))(buf)
91}
92
93fn rset(buf: &[u8]) -> IResult<&[u8], Cmd> {
94    value(Cmd::Rset, tag_no_case(b"rset"))(buf)
95}
96
97fn quit(buf: &[u8]) -> IResult<&[u8], Cmd> {
98    value(Cmd::Quit, tag_no_case(b"quit"))(buf)
99}
100
101fn vrfy(buf: &[u8]) -> IResult<&[u8], Cmd> {
102    let preamble = preceded(cmd(b"vrfy"), take_all);
103    value(Cmd::Vrfy, preamble)(buf)
104}
105
106fn noop(buf: &[u8]) -> IResult<&[u8], Cmd> {
107    value(Cmd::Noop, tag_no_case(b"noop"))(buf)
108}
109
110fn starttls(buf: &[u8]) -> IResult<&[u8], Cmd> {
111    value(Cmd::StartTls, tag_no_case(b"starttls"))(buf)
112}
113
114fn is_base64(chr: u8) -> bool {
115    is_alphanumeric(chr) || (chr == b'+') || (chr == b'/' || chr == b'=')
116}
117
118fn auth_initial(buf: &[u8]) -> IResult<&[u8], &[u8]> {
119    preceded(space, take_while1(is_base64))(buf)
120}
121
122fn auth_response(buf: &[u8]) -> IResult<&[u8], &[u8]> {
123    terminated(take_while1(is_base64), tag("\r\n"))(buf)
124}
125
126fn empty(buf: &[u8]) -> IResult<&[u8], &[u8]> {
127    Ok((buf, b"" as &[u8]))
128}
129
130fn auth_plain(buf: &[u8]) -> IResult<&[u8], Cmd> {
131    let parser = preceded(tag_no_case(b"plain"), alt((auth_initial, empty)));
132    map(parser, sasl_plain_cmd)(buf)
133}
134
135fn auth_login(buf: &[u8]) -> IResult<&[u8], Cmd> {
136    let parser = preceded(tag_no_case(b"login"), alt((auth_initial, empty)));
137    map(parser, sasl_login_cmd)(buf)
138}
139
140fn auth(buf: &[u8]) -> IResult<&[u8], Cmd> {
141    preceded(cmd(b"auth"), alt((auth_plain, auth_login)))(buf)
142}
143
144//---- Helper functions ---------------------------------------------------------
145
146// Return a parser to match the given command
147fn cmd(cmd_tag: &[u8]) -> impl Fn(&[u8]) -> IResult<&[u8], (&[u8], &[u8])> + '_ {
148    move |buf: &[u8]| pair(tag_no_case(cmd_tag), space)(buf)
149}
150
151// Match one or more spaces
152fn space(buf: &[u8]) -> IResult<&[u8], &[u8]> {
153    take_while1(|b| b == b' ')(buf)
154}
155
156fn sasl_plain_cmd(param: &[u8]) -> Cmd {
157    if param.is_empty() {
158        Cmd::AuthPlainEmpty
159    } else {
160        let creds = decode_sasl_plain(param);
161        Cmd::AuthPlain {
162            authorization_id: creds.authorization_id,
163            authentication_id: creds.authentication_id,
164            password: creds.password,
165        }
166    }
167}
168
169fn sasl_login_cmd(param: &[u8]) -> Cmd {
170    if param.is_empty() {
171        Cmd::AuthLoginEmpty
172    } else {
173        Cmd::AuthLogin {
174            username: decode_sasl_login(param),
175        }
176    }
177}
178
179// Decodes the base64 encoded plain authentication parameter
180pub(crate) fn decode_sasl_plain(param: &[u8]) -> Credentials {
181    let decoded = base64::decode(param);
182    if let Ok(bytes) = decoded {
183        let mut fields = bytes.split(|b| b == &0u8);
184        let authorization_id = next_string(&mut fields);
185        let authentication_id = next_string(&mut fields);
186        let password = next_string(&mut fields);
187        Credentials {
188            authorization_id,
189            authentication_id,
190            password,
191        }
192    } else {
193        Credentials {
194            authorization_id: String::default(),
195            authentication_id: String::default(),
196            password: String::default(),
197        }
198    }
199}
200
201// Decodes base64 encoded login authentication parameters (in login auth, username and password are
202// sent in separate lines)
203pub(crate) fn decode_sasl_login(param: &[u8]) -> String {
204    let decoded = base64::decode(param).unwrap_or_default();
205    String::from_utf8(decoded).unwrap_or_default()
206}
207
208fn next_string(it: &mut dyn Iterator<Item = &[u8]>) -> String {
209    it.next()
210        .map(|s| str::from_utf8(s).unwrap_or_default())
211        .unwrap_or_default()
212        .to_owned()
213}
214
215//---- Tests --------------------------------------------------------------------
216
217mod tests {
218    #[allow(unused_imports)]
219    use super::*;
220
221    #[test]
222    fn auth_initial_plain() {
223        let res = parse(b"auth plain dGVzdAB0ZXN0ADEyMzQ=\r\n");
224        match res {
225            Ok(Cmd::AuthPlain {
226                authorization_id,
227                authentication_id,
228                password,
229            }) => {
230                assert_eq!(authorization_id, "test");
231                assert_eq!(authentication_id, "test");
232                assert_eq!(password, "1234");
233            }
234            _ => panic!("Auth plain with initial response incorrectly parsed"),
235        };
236    }
237
238    #[test]
239    fn auth_initial_login() {
240        let res = parse(b"auth login ZHVtbXk=\r\n");
241        match res {
242            Ok(Cmd::AuthLogin { username }) => {
243                assert_eq!(username, "dummy");
244            }
245            _ => panic!("Auth login with initial response incorrectly parsed"),
246        };
247    }
248
249    #[test]
250    fn auth_empty_plain() {
251        let res = parse(b"auth plain\r\n");
252        match res {
253            Ok(Cmd::AuthPlainEmpty) => {}
254            _ => panic!("Auth plain without initial response incorrectly parsed"),
255        };
256    }
257
258    #[test]
259    fn auth_empty_login() {
260        let res = parse(b"auth login\r\n");
261        match res {
262            Ok(Cmd::AuthLoginEmpty) => {}
263            _ => panic!("Auth login without initial response incorrectly parsed"),
264        };
265    }
266}