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
12pub 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
23pub 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
144fn 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
151fn 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
179pub(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
201pub(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
215mod 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}