smtp_codec/parse/
imf.rs

1//! Internet Message Format (RFC 5322)
2//!
3//! TODO: replace this with an IMF library, e.g. rustyknife?
4
5/// 3.2.1.  Quoted characters
6pub mod quoted_characters {
7    use abnf_core::streaming::{is_VCHAR, WSP};
8    use nom::{
9        branch::alt,
10        bytes::streaming::{tag, take_while_m_n},
11        combinator::recognize,
12        sequence::tuple,
13        IResult,
14    };
15
16    use super::obsolete::obs_qp;
17
18    /// quoted-pair = ("\" (VCHAR / WSP)) / obs-qp
19    pub fn quoted_pair(input: &[u8]) -> IResult<&[u8], &[u8]> {
20        let parser = alt((
21            recognize(tuple((
22                tag(b"\\"),
23                alt((take_while_m_n(1, 1, is_VCHAR), WSP)),
24            ))),
25            obs_qp,
26        ));
27
28        let (remaining, parsed) = recognize(parser)(input)?;
29
30        Ok((remaining, parsed))
31    }
32}
33
34/// 3.2.2.  Folding White Space and Comments
35pub mod folding_ws_and_comment {
36    use nom::IResult;
37
38    /// Folding white space
39    ///
40    /// FWS = ([*WSP CRLF] 1*WSP) / obs-FWS
41    pub fn FWS(_input: &[u8]) -> IResult<&[u8], &[u8]> {
42        unimplemented!()
43    }
44
45    // Printable US-ASCII characters not including "(", ")", or "\"
46    //
47    // ctext = %d33-39 / %d42-91 / %d93-126 / obs-ctext
48
49    // ccontent = ctext / quoted-pair / comment
50
51    // comment = "(" *([FWS] ccontent) [FWS] ")"
52
53    /// CFWS = (1*([FWS] comment) [FWS]) / FWS
54    pub fn CFWS(_input: &[u8]) -> IResult<&[u8], &[u8]> {
55        unimplemented!()
56    }
57}
58
59/// 3.2.3.  Atom
60pub mod atom {
61    use abnf_core::streaming::{is_ALPHA, is_DIGIT};
62    use nom::{
63        bytes::streaming::{tag, take_while1},
64        combinator::{opt, recognize},
65        multi::many0,
66        sequence::tuple,
67        IResult,
68    };
69
70    use super::folding_ws_and_comment::CFWS;
71
72    /// Printable US-ASCII characters not including specials.
73    /// Used for atoms.
74    ///
75    /// atext = ALPHA / DIGIT /
76    ///          "!" / "#" /
77    ///          "$" / "%" /
78    ///          "&" / "'" /
79    ///          "*" / "+" /
80    ///          "-" / "/" /
81    ///          "=" / "?" /
82    ///          "^" / "_" /
83    ///          "`" / "{" /
84    ///          "|" / "}" /
85    ///          "~"
86    pub fn is_atext(byte: u8) -> bool {
87        let allowed = b"!#$%&'*+-/=?^_`{|}~";
88
89        is_ALPHA(byte) || is_DIGIT(byte) || allowed.contains(&byte)
90    }
91
92    /// atom = [CFWS] 1*atext [CFWS]
93    pub fn atom(input: &[u8]) -> IResult<&[u8], &[u8]> {
94        let parser = tuple((opt(CFWS), take_while1(is_atext), opt(CFWS)));
95
96        let (remaining, parsed) = recognize(parser)(input)?;
97
98        Ok((remaining, parsed))
99    }
100
101    /// dot-atom-text = 1*atext *("." 1*atext)
102    pub fn dot_atom_text(input: &[u8]) -> IResult<&[u8], &[u8]> {
103        let parser = tuple((
104            take_while1(is_atext),
105            many0(tuple((tag(b"."), take_while1(is_atext)))),
106        ));
107
108        let (remaining, parsed) = recognize(parser)(input)?;
109
110        Ok((remaining, parsed))
111    }
112
113    // dot-atom = [CFWS] dot-atom-text [CFWS]
114    pub fn dot_atom(input: &[u8]) -> IResult<&[u8], &[u8]> {
115        let parser = tuple((opt(CFWS), dot_atom_text, opt(CFWS)));
116
117        let (remaining, parsed) = recognize(parser)(input)?;
118
119        Ok((remaining, parsed))
120    }
121
122    // Special characters that do not appear in atext.
123    //
124    // specials = "(" / ")" /
125    //            "<" / ">" /
126    //            "[" / "]" /
127    //            ":" / ";" /
128    //            "@" / "\" /
129    //            "," / "." /
130    //            DQUOTE
131    // ...
132}
133
134/// 3.2.4.  Quoted Strings
135pub mod quoted_strings {
136    use abnf_core::streaming::DQUOTE;
137    use nom::{
138        branch::alt,
139        bytes::streaming::take_while_m_n,
140        combinator::{opt, recognize},
141        multi::many0,
142        sequence::tuple,
143        IResult,
144    };
145
146    use super::{
147        folding_ws_and_comment::{CFWS, FWS},
148        obsolete::is_obs_qtext,
149        quoted_characters::quoted_pair,
150    };
151
152    /// Printable US-ASCII characters not including "\" or the quote character.
153    ///
154    /// qtext = %d33 / %d35-91 / %d93-126 / obs-qtext
155    pub fn is_qtext(byte: u8) -> bool {
156        match byte {
157            33 | 35..=91 | 93..=126 => true,
158            _ if is_obs_qtext(byte) => true,
159            _ => false,
160        }
161    }
162
163    /// qcontent = qtext / quoted-pair
164    pub fn qcontent(input: &[u8]) -> IResult<&[u8], &[u8]> {
165        let parser = alt((take_while_m_n(1, 1, is_qtext), quoted_pair));
166
167        let (remaining, parsed) = recognize(parser)(input)?;
168
169        Ok((remaining, parsed))
170    }
171
172    /// quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS]
173    pub fn quoted_string(input: &[u8]) -> IResult<&[u8], &[u8]> {
174        let parser = tuple((
175            opt(CFWS),
176            DQUOTE,
177            many0(tuple((opt(FWS), qcontent))),
178            opt(FWS),
179            DQUOTE,
180            opt(CFWS),
181        ));
182
183        let (remaining, parsed) = recognize(parser)(input)?;
184
185        Ok((remaining, parsed))
186    }
187}
188
189/// 3.2.5.  Miscellaneous Tokens
190pub mod miscellaneous {
191    use nom::{branch::alt, IResult};
192
193    use super::{atom::atom, quoted_strings::quoted_string};
194
195    /// word = atom / quoted-string
196    pub fn word(input: &[u8]) -> IResult<&[u8], &[u8]> {
197        alt((atom, quoted_string))(input)
198    }
199
200    // phrase = 1*word / obs-phrase
201    // ...
202
203    // unstructured = (*([FWS] VCHAR) *WSP) / obs-unstruct
204    // ...
205}
206
207/// 3.3.  Date and Time Specification
208pub mod datetime {
209    use abnf_core::streaming::is_DIGIT;
210    use nom::{
211        branch::alt,
212        bytes::streaming::{tag, tag_no_case, take_while_m_n},
213        combinator::{opt, recognize},
214        sequence::tuple,
215        IResult,
216    };
217
218    use super::folding_ws_and_comment::{CFWS, FWS};
219
220    // date-time = [ day-of-week "," ] date time [CFWS]
221    pub fn date_time(input: &[u8]) -> IResult<&[u8], &[u8]> {
222        let parser = tuple((opt(tuple((day_of_week, tag(b",")))), date, time, opt(CFWS)));
223
224        let (remaining, parsed) = recognize(parser)(input)?;
225
226        Ok((remaining, parsed))
227    }
228
229    // day-of-week = ([FWS] day-name) / obs-day-of-week
230    pub fn day_of_week(input: &[u8]) -> IResult<&[u8], &[u8]> {
231        let parser = tuple((opt(FWS), day_name));
232
233        let (remaining, parsed) = recognize(parser)(input)?;
234
235        Ok((remaining, parsed))
236    }
237
238    // day-name = "Mon" / "Tue" / "Wed" / "Thu" / "Fri" / "Sat" / "Sun"
239    pub fn day_name(input: &[u8]) -> IResult<&[u8], &[u8]> {
240        let parser = alt((
241            tag_no_case(b"Mon"),
242            tag_no_case(b"Tue"),
243            tag_no_case(b"Wed"),
244            tag_no_case(b"Thu"),
245            tag_no_case(b"Fri"),
246            tag_no_case(b"Sat"),
247            tag_no_case(b"Sun"),
248        ));
249
250        let (remaining, parsed) = recognize(parser)(input)?;
251
252        Ok((remaining, parsed))
253    }
254
255    // date = day month year
256    pub fn date(input: &[u8]) -> IResult<&[u8], &[u8]> {
257        let parser = tuple((day, month, year));
258
259        let (remaining, parsed) = recognize(parser)(input)?;
260
261        Ok((remaining, parsed))
262    }
263
264    // day = ([FWS] 1*2DIGIT FWS) / obs-day
265    pub fn day(input: &[u8]) -> IResult<&[u8], &[u8]> {
266        let parser = tuple((opt(FWS), take_while_m_n(1, 2, is_DIGIT), FWS));
267
268        let (remaining, parsed) = recognize(parser)(input)?;
269
270        Ok((remaining, parsed))
271    }
272
273    // month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" / "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
274    pub fn month(input: &[u8]) -> IResult<&[u8], &[u8]> {
275        let parser = alt((
276            tag_no_case(b"Jan"),
277            tag_no_case(b"Feb"),
278            tag_no_case(b"Mar"),
279            tag_no_case(b"Apr"),
280            tag_no_case(b"May"),
281            tag_no_case(b"Jun"),
282            tag_no_case(b"Jul"),
283            tag_no_case(b"Aug"),
284            tag_no_case(b"Sep"),
285            tag_no_case(b"Oct"),
286            tag_no_case(b"Nov"),
287            tag_no_case(b"Dec"),
288        ));
289
290        let (remaining, parsed) = recognize(parser)(input)?;
291
292        Ok((remaining, parsed))
293    }
294
295    // year = (FWS 4*DIGIT FWS) / obs-year
296    pub fn year(input: &[u8]) -> IResult<&[u8], &[u8]> {
297        let parser = tuple((FWS, take_while_m_n(4, 8, is_DIGIT), FWS)); // FIXME: 4*?!
298
299        let (remaining, parsed) = recognize(parser)(input)?;
300
301        Ok((remaining, parsed))
302    }
303
304    // time = time-of-day zone
305    pub fn time(input: &[u8]) -> IResult<&[u8], &[u8]> {
306        let parser = tuple((time_of_day, zone));
307
308        let (remaining, parsed) = recognize(parser)(input)?;
309
310        Ok((remaining, parsed))
311    }
312
313    // time-of-day = hour ":" minute [ ":" second ]
314    pub fn time_of_day(input: &[u8]) -> IResult<&[u8], &[u8]> {
315        let parser = tuple((hour, tag(b":"), minute, opt(tuple((tag(b":"), second)))));
316
317        let (remaining, parsed) = recognize(parser)(input)?;
318
319        Ok((remaining, parsed))
320    }
321
322    // hour = 2DIGIT / obs-hour
323    pub fn hour(input: &[u8]) -> IResult<&[u8], &[u8]> {
324        // FIXME: obs- forms must not be used in SMTP. Never?
325
326        let parser = take_while_m_n(2, 2, is_DIGIT);
327
328        let (remaining, parsed) = recognize(parser)(input)?;
329
330        Ok((remaining, parsed))
331    }
332
333    // minute = 2DIGIT / obs-minute
334    pub fn minute(input: &[u8]) -> IResult<&[u8], &[u8]> {
335        // FIXME: obs- forms must not be used in SMTP. Never?
336
337        let parser = take_while_m_n(2, 2, is_DIGIT);
338
339        let (remaining, parsed) = recognize(parser)(input)?;
340
341        Ok((remaining, parsed))
342    }
343
344    // second = 2DIGIT / obs-second
345    pub fn second(input: &[u8]) -> IResult<&[u8], &[u8]> {
346        // FIXME: obs- forms must not be used in SMTP. Never?
347
348        let parser = take_while_m_n(2, 2, is_DIGIT);
349
350        let (remaining, parsed) = recognize(parser)(input)?;
351
352        Ok((remaining, parsed))
353    }
354
355    // zone = (FWS ( "+" / "-" ) 4DIGIT) / obs-zone
356    pub fn zone(input: &[u8]) -> IResult<&[u8], &[u8]> {
357        // FIXME: obs- forms must not be used in SMTP. Never?
358
359        let parser = tuple((
360            FWS,
361            alt((tag(b"+"), tag(b"-"))),
362            take_while_m_n(4, 4, is_DIGIT),
363        ));
364
365        let (remaining, parsed) = recognize(parser)(input)?;
366
367        Ok((remaining, parsed))
368    }
369}
370
371/// 3.4.1.  Addr-Spec Specification
372pub mod addr_spec {
373    use nom::{
374        branch::alt,
375        bytes::streaming::{tag, take_while_m_n},
376        combinator::{opt, recognize},
377        multi::many0,
378        sequence::tuple,
379        IResult,
380    };
381
382    use super::{
383        atom::dot_atom,
384        folding_ws_and_comment::{CFWS, FWS},
385        obsolete::{obs_domain, obs_dtext, obs_local_part},
386        quoted_strings::quoted_string,
387    };
388
389    // addr-spec = local-part "@" domain
390
391    /// local-part = dot-atom / quoted-string / obs-local-part
392    pub fn local_part(input: &[u8]) -> IResult<&[u8], &[u8]> {
393        let parser = alt((dot_atom, quoted_string, obs_local_part));
394
395        let (remaining, parsed) = recognize(parser)(input)?;
396
397        Ok((remaining, parsed))
398    }
399
400    /// domain = dot-atom / domain-literal / obs-domain
401    pub fn domain(input: &[u8]) -> IResult<&[u8], &[u8]> {
402        let parser = alt((dot_atom, domain_literal, obs_domain));
403
404        let (remaining, parsed) = recognize(parser)(input)?;
405
406        Ok((remaining, parsed))
407    }
408
409    /// domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
410    pub fn domain_literal(input: &[u8]) -> IResult<&[u8], &[u8]> {
411        let parser = tuple((
412            opt(CFWS),
413            tag(b"["),
414            many0(tuple((opt(FWS), dtext))),
415            opt(FWS),
416            tag(b"]"),
417            opt(CFWS),
418        ));
419
420        let (remaining, parsed) = recognize(parser)(input)?;
421
422        Ok((remaining, parsed))
423    }
424
425    /// Printable US-ASCII characters not including "[", "]", or "\".
426    ///
427    /// dtext = %d33-90 / %d94-126 / obs-dtext
428    pub fn dtext(input: &[u8]) -> IResult<&[u8], &[u8]> {
429        fn is_a(byte: u8) -> bool {
430            matches!(byte, 33..=90)
431        }
432
433        fn is_b(byte: u8) -> bool {
434            matches!(byte, 94..=126)
435        }
436
437        let parser = alt((
438            take_while_m_n(1, 1, is_a),
439            take_while_m_n(1, 1, is_b),
440            obs_dtext,
441        ));
442
443        let (remaining, parsed) = recognize(parser)(input)?;
444
445        Ok((remaining, parsed))
446    }
447}
448
449/// 3.6.4.  Identification Fields
450pub mod identification {
451    use nom::{
452        branch::alt,
453        bytes::streaming::tag,
454        combinator::{opt, recognize},
455        multi::many0,
456        sequence::{delimited, tuple},
457        IResult,
458    };
459
460    use super::{
461        addr_spec::dtext,
462        atom::dot_atom_text,
463        folding_ws_and_comment::CFWS,
464        obsolete::{obs_id_left, obs_id_right},
465    };
466
467    // message-id = "Message-ID:" msg-id CRLF
468    // ...
469
470    // in-reply-to = "In-Reply-To:" 1*msg-id CRLF
471    // ...
472
473    // references = "References:" 1*msg-id CRLF
474    // ...
475
476    /// msg-id = [CFWS] "<" id-left "@" id-right ">" [CFWS]
477    pub fn msg_id(input: &[u8]) -> IResult<&[u8], &[u8]> {
478        let parser = tuple((
479            opt(CFWS),
480            tag(b"<"),
481            id_left,
482            tag(b"@"),
483            id_right,
484            tag(b">"),
485            opt(CFWS),
486        ));
487
488        let (remaining, parsed) = recognize(parser)(input)?;
489
490        Ok((remaining, parsed))
491    }
492
493    /// id-left = dot-atom-text / obs-id-left
494    pub fn id_left(input: &[u8]) -> IResult<&[u8], &[u8]> {
495        let parser = alt((dot_atom_text, obs_id_left));
496
497        let (remaining, parsed) = recognize(parser)(input)?;
498
499        Ok((remaining, parsed))
500    }
501
502    /// id-right = dot-atom-text / no-fold-literal / obs-id-right
503    pub fn id_right(input: &[u8]) -> IResult<&[u8], &[u8]> {
504        let parser = alt((dot_atom_text, no_fold_literal, obs_id_right));
505
506        let (remaining, parsed) = recognize(parser)(input)?;
507
508        Ok((remaining, parsed))
509    }
510
511    // no-fold-literal = "[" *dtext "]"
512    pub fn no_fold_literal(input: &[u8]) -> IResult<&[u8], &[u8]> {
513        let parser = delimited(tag(b"["), many0(dtext), tag(b"]"));
514
515        let (remaining, parsed) = recognize(parser)(input)?;
516
517        Ok((remaining, parsed))
518    }
519}
520
521/// 4.1.  Miscellaneous Obsolete Tokens
522pub mod obsolete {
523    use abnf_core::streaming::{CR, LF};
524    use nom::{
525        branch::alt,
526        bytes::streaming::{tag, take_while_m_n},
527        combinator::recognize,
528        multi::many0,
529        sequence::tuple,
530        IResult,
531    };
532
533    use super::{
534        addr_spec::{domain, local_part},
535        atom::atom,
536        miscellaneous::word,
537        quoted_characters::quoted_pair,
538    };
539
540    /// US-ASCII control characters that do not include the carriage
541    /// return, line feed, and white space characters
542    ///
543    /// obs-NO-WS-CTL = %d1-8 / %d11 / %d12 / %d14-31 / %d127
544    pub fn is_obs_NO_WS_CTL(byte: u8) -> bool {
545        matches!(byte, 1..=8 | 11 | 12 | 14..=31 | 127)
546    }
547
548    /// obs-qtext = obs-NO-WS-CTL
549    pub fn is_obs_qtext(byte: u8) -> bool {
550        is_obs_NO_WS_CTL(byte)
551    }
552
553    /// obs-qp = "\" (%d0 / obs-NO-WS-CTL / LF / CR)
554    pub fn obs_qp(input: &[u8]) -> IResult<&[u8], &[u8]> {
555        let parser = tuple((
556            tag(b"\\"),
557            alt((
558                take_while_m_n(1, 1, |x| x == 0x00),
559                take_while_m_n(1, 1, is_obs_NO_WS_CTL),
560                LF,
561                CR,
562            )),
563        ));
564
565        let (remaining, parsed) = recognize(parser)(input)?;
566
567        Ok((remaining, parsed))
568    }
569
570    // 4.4.  Obsolete Addressing (RFC 5322)
571
572    /// obs-local-part = word *("." word)
573    pub fn obs_local_part(input: &[u8]) -> IResult<&[u8], &[u8]> {
574        let parser = tuple((word, many0(tuple((tag(b"."), word)))));
575
576        let (remaining, parsed) = recognize(parser)(input)?;
577
578        Ok((remaining, parsed))
579    }
580
581    /// obs-domain = atom *("." atom)
582    pub fn obs_domain(input: &[u8]) -> IResult<&[u8], &[u8]> {
583        let parser = tuple((atom, many0(tuple((tag(b"."), atom)))));
584
585        let (remaining, parsed) = recognize(parser)(input)?;
586
587        Ok((remaining, parsed))
588    }
589
590    /// obs-dtext = obs-NO-WS-CTL / quoted-pair
591    pub fn obs_dtext(input: &[u8]) -> IResult<&[u8], &[u8]> {
592        let parser = alt((take_while_m_n(1, 1, is_obs_NO_WS_CTL), quoted_pair));
593
594        let (remaining, parsed) = recognize(parser)(input)?;
595
596        Ok((remaining, parsed))
597    }
598
599    // 4.5.4.  Obsolete Identification Fields (RFC 5322)
600
601    /// obs-id-left = local-part
602    pub fn obs_id_left(input: &[u8]) -> IResult<&[u8], &[u8]> {
603        local_part(input)
604    }
605
606    /// obs-id-right = domain
607    pub fn obs_id_right(input: &[u8]) -> IResult<&[u8], &[u8]> {
608        domain(input)
609    }
610}