1use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, SP};
2use nom::{
3 branch::alt,
4 bytes::streaming::{tag, tag_no_case, take_while, take_while1, take_while_m_n},
5 combinator::{map, map_res, opt, recognize, value},
6 multi::{many0, separated_list0},
7 sequence::{delimited, preceded, tuple},
8 IResult,
9};
10
11use crate::{
12 parse::{address::address_literal, number, Domain},
13 types::{AuthMechanism, Capability, Response},
14};
15
16pub fn Greeting(input: &[u8]) -> IResult<&[u8], Response> {
21 let mut parser = alt((
22 map(
23 tuple((
24 tag(b"220 "),
25 alt((Domain, address_literal)),
26 opt(preceded(SP, textstring)),
27 CRLF,
28 )),
29 |(_, domain, maybe_text, _)| Response::Greeting {
30 domain: domain.to_owned(),
31 text: maybe_text
32 .map(|str| str.to_string())
33 .unwrap_or_else(|| "".to_string()),
34 },
35 ),
36 map(
37 tuple((
38 tag(b"220-"),
39 alt((Domain, address_literal)),
40 opt(preceded(SP, textstring)),
41 CRLF,
42 many0(delimited(tag(b"220-"), opt(textstring), CRLF)),
43 tag(b"220"),
44 opt(preceded(SP, textstring)),
45 CRLF,
46 )),
47 |(_, domain, maybe_text, _, more_text, _, moar_text, _)| Response::Greeting {
48 domain: domain.to_owned(),
49 text: {
50 let mut res = maybe_text
51 .map(|str| format!("{}\n", str))
52 .unwrap_or_else(|| "\n".to_string());
53
54 for text in more_text {
55 let text = text
56 .map(|str| format!("{}\n", str))
57 .unwrap_or_else(|| "\n".to_string());
58 res.push_str(&text);
59 }
60
61 let text = moar_text
62 .map(|str| str.to_string())
63 .unwrap_or_else(|| "".to_string());
64 res.push_str(&text);
65
66 res
67 },
68 },
69 ),
70 ));
71
72 let (remaining, parsed) = parser(input)?;
73
74 Ok((remaining, parsed))
75}
76
77pub fn textstring(input: &[u8]) -> IResult<&[u8], &str> {
81 fn is_value(byte: u8) -> bool {
82 matches!(byte, 9 | 32..=126)
83 }
84
85 let (remaining, parsed) = map_res(take_while1(is_value), std::str::from_utf8)(input)?;
86
87 Ok((remaining, parsed))
88}
89
90pub fn Reply_line(input: &[u8]) -> IResult<&[u8], &[u8]> {
95 let parser = tuple((
96 many0(tuple((Reply_code, tag(b"-"), opt(textstring), CRLF))),
97 Reply_code,
98 opt(tuple((SP, textstring))),
99 CRLF,
100 ));
101
102 let (remaining, parsed) = recognize(parser)(input)?;
103
104 Ok((remaining, parsed))
105}
106
107pub fn Reply_code(input: &[u8]) -> IResult<&[u8], u16> {
113 map_res(
115 map_res(
116 take_while_m_n(3, 3, nom::character::is_digit),
117 std::str::from_utf8,
118 ),
119 |s| u16::from_str_radix(s, 10),
120 )(input)
121}
122
123pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], Response> {
132 let mut parser = alt((
133 map(
134 tuple((tag(b"250 "), Domain, opt(preceded(SP, ehlo_greet)), CRLF)),
135 |(_, domain, maybe_ehlo, _)| Response::Ehlo {
136 domain: domain.to_owned(),
137 greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()),
138 capabilities: Vec::new(),
139 },
140 ),
141 map(
142 tuple((
143 tag(b"250-"),
144 Domain,
145 opt(preceded(SP, ehlo_greet)),
146 CRLF,
147 many0(delimited(tag(b"250-"), ehlo_line, CRLF)),
148 tag(b"250 "),
149 ehlo_line,
150 CRLF,
151 )),
152 |(_, domain, maybe_ehlo, _, mut lines, _, line, _)| Response::Ehlo {
153 domain: domain.to_owned(),
154 greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()),
155 capabilities: {
156 lines.push(line);
157 lines
158 },
159 },
160 ),
161 ));
162
163 let (remaining, parsed) = parser(input)?;
164
165 Ok((remaining, parsed))
166}
167
168pub fn ehlo_greet(input: &[u8]) -> IResult<&[u8], &str> {
172 fn is_valid_character(byte: u8) -> bool {
173 matches!(byte, 0..=9 | 11..=12 | 14..=127)
174 }
175
176 map_res(take_while1(is_valid_character), std::str::from_utf8)(input)
177}
178
179pub fn ehlo_line(input: &[u8]) -> IResult<&[u8], Capability> {
183 let auth = tuple((
184 tag_no_case("AUTH"),
185 alt((tag_no_case(" "), tag_no_case("="))),
186 separated_list0(SP, auth_mechanism),
187 ));
188
189 let other = tuple((
190 map_res(ehlo_keyword, std::str::from_utf8),
191 opt(preceded(
192 alt((SP, tag("="))), separated_list0(SP, ehlo_param),
194 )),
195 ));
196
197 alt((
198 value(Capability::EXPN, tag_no_case("EXPN")),
199 value(Capability::Help, tag_no_case("HELP")),
200 value(Capability::EightBitMIME, tag_no_case("8BITMIME")),
201 map(preceded(tag_no_case("SIZE "), number), Capability::Size),
202 value(Capability::Chunking, tag_no_case("CHUNKING")),
203 value(Capability::BinaryMIME, tag_no_case("BINARYMIME")),
204 value(Capability::Checkpoint, tag_no_case("CHECKPOINT")),
205 value(Capability::DeliverBy, tag_no_case("DELIVERBY")),
206 value(Capability::Pipelining, tag_no_case("PIPELINING")),
207 value(Capability::DSN, tag_no_case("DSN")),
208 value(Capability::ETRN, tag_no_case("ETRN")),
209 value(
210 Capability::EnhancedStatusCodes,
211 tag_no_case("ENHANCEDSTATUSCODES"),
212 ),
213 value(Capability::StartTLS, tag_no_case("STARTTLS")),
214 value(Capability::MTRK, tag_no_case("MTRK")),
216 value(Capability::ATRN, tag_no_case("ATRN")),
217 map(auth, |(_, _, mechanisms)| Capability::Auth(mechanisms)),
218 value(Capability::BURL, tag_no_case("BURL")),
219 value(Capability::SMTPUTF8, tag_no_case("SMTPUTF8")),
223 value(Capability::RRVS, tag_no_case("RRVS")),
225 value(Capability::RequireTLS, tag_no_case("REQUIRETLS")),
226 map(other, |(keyword, params)| Capability::Other {
227 keyword: keyword.into(),
228 params: params
229 .map(|v| v.iter().map(|s| s.to_string()).collect())
230 .unwrap_or_default(),
231 }),
232 ))(input)
233}
234
235pub fn ehlo_keyword(input: &[u8]) -> IResult<&[u8], &[u8]> {
239 let parser = tuple((
240 take_while_m_n(1, 1, |byte| is_ALPHA(byte) || is_DIGIT(byte)),
241 take_while(|byte| is_ALPHA(byte) || is_DIGIT(byte) || byte == b'-'),
242 ));
243
244 let (remaining, parsed) = recognize(parser)(input)?;
245
246 Ok((remaining, parsed))
247}
248
249pub fn ehlo_param(input: &[u8]) -> IResult<&[u8], &str> {
254 fn is_valid_character(byte: u8) -> bool {
255 matches!(byte, 33..=126)
256 }
257
258 map_res(take_while1(is_valid_character), std::str::from_utf8)(input)
259}
260
261pub fn auth_mechanism(input: &[u8]) -> IResult<&[u8], AuthMechanism> {
262 alt((
263 value(AuthMechanism::Login, tag_no_case("LOGIN")),
264 value(AuthMechanism::Plain, tag_no_case("PLAIN")),
265 value(AuthMechanism::CramMD5, tag_no_case("CRAM-MD5")),
266 value(AuthMechanism::CramSHA1, tag_no_case("CRAM-SHA1")),
267 value(AuthMechanism::DigestMD5, tag_no_case("DIGEST-MD5")),
268 value(AuthMechanism::ScramMD5, tag_no_case("SCRAM-MD5")),
269 value(AuthMechanism::GSSAPI, tag_no_case("GSSAPI")),
270 value(AuthMechanism::NTLM, tag_no_case("NTLM")),
271 map(ehlo_param, |param| AuthMechanism::Other(param.to_string())),
272 ))(input)
273}
274
275#[cfg(test)]
278mod test {
279 use super::*;
280 use crate::types::AuthMechanism;
281
282 #[test]
283 fn test_Greeting() {
284 let greeting = b"220-example.org ESMTP Fake 4.93 #2 Thu, 16 Jul 2020 07:30:16 -0400\r\n\
285220-We do not authorize the use of this system to transport unsolicited,\r\n\
286220 and/or bulk e-mail.\r\n";
287
288 let (rem, out) = Greeting(greeting).unwrap();
289 assert_eq!(rem, b"");
290 assert_eq!(
291 out,
292 Response::Greeting {
293 domain: "example.org".into(),
294 text: "ESMTP Fake 4.93 #2 Thu, 16 Jul 2020 07:30:16 -0400\n\
295We do not authorize the use of this system to transport unsolicited,\n\
296and/or bulk e-mail."
297 .into(),
298 }
299 )
300 }
301
302 #[test]
303 fn test_ehlo_ok_rsp() {
304 let (rem, out) = ehlo_ok_rsp(
305 b"250-example.org hello\r\n\
306250-AUTH LOGIN CRAM-MD5 PLAIN\r\n\
307250-AUTH=LOGIN CRAM-MD5 PLAIN\r\n\
308250-STARTTLS\r\n\
309250-SIZE 12345\r\n\
310250 8BITMIME\r\n",
311 )
312 .unwrap();
313 assert_eq!(rem, b"");
314 assert_eq!(
315 out,
316 Response::Ehlo {
317 domain: "example.org".into(),
318 greet: Some("hello".into()),
319 capabilities: vec![
320 Capability::Auth(vec![
321 AuthMechanism::Login,
322 AuthMechanism::CramMD5,
323 AuthMechanism::Plain
324 ]),
325 Capability::Auth(vec![
326 AuthMechanism::Login,
327 AuthMechanism::CramMD5,
328 AuthMechanism::Plain
329 ]),
330 Capability::StartTLS,
331 Capability::Size(12345),
332 Capability::EightBitMIME,
333 ],
334 }
335 );
336 }
337
338 #[test]
339 fn test_ehlo_line() {
340 let (rem, capability) = ehlo_line(b"SIZE 123456\r\n").unwrap();
341 assert_eq!(rem, b"\r\n");
342 assert_eq!(capability, Capability::Size(123456));
343 }
344}