imap_codec/
fetch.rs

1use std::num::NonZeroU32;
2
3use abnf_core::streaming::sp;
4use imap_types::{
5    core::{AString, NonEmptyVec},
6    fetch::{MessageDataItem, MessageDataItemName, Part, PartSpecifier, Section},
7};
8use nom::{
9    branch::alt,
10    bytes::streaming::{tag, tag_no_case},
11    combinator::{map, opt, value},
12    multi::separated_list1,
13    sequence::{delimited, tuple},
14};
15
16use crate::{
17    body::body,
18    core::{astring, nstring, number, nz_number},
19    datetime::date_time,
20    decode::IMAPResult,
21    envelope::envelope,
22    flag::flag_fetch,
23};
24
25/// `fetch-att = "ENVELOPE" /
26///              "FLAGS" /
27///              "INTERNALDATE" /
28///              "RFC822" [".HEADER" / ".SIZE" / ".TEXT"] /
29///              "BODY" ["STRUCTURE"] /
30///              "UID" /
31///              "BODY" section ["<" number "." nz-number ">"] /
32///              "BODY.PEEK" section ["<" number "." nz-number ">"]`
33pub(crate) fn fetch_att(input: &[u8]) -> IMAPResult<&[u8], MessageDataItemName> {
34    alt((
35        value(MessageDataItemName::Envelope, tag_no_case(b"ENVELOPE")),
36        value(MessageDataItemName::Flags, tag_no_case(b"FLAGS")),
37        value(
38            MessageDataItemName::InternalDate,
39            tag_no_case(b"INTERNALDATE"),
40        ),
41        value(
42            MessageDataItemName::BodyStructure,
43            tag_no_case(b"BODYSTRUCTURE"),
44        ),
45        map(
46            tuple((
47                tag_no_case(b"BODY.PEEK"),
48                section,
49                opt(delimited(
50                    tag(b"<"),
51                    tuple((number, tag(b"."), nz_number)),
52                    tag(b">"),
53                )),
54            )),
55            |(_, section, byterange)| MessageDataItemName::BodyExt {
56                section,
57                partial: byterange.map(|(start, _, end)| (start, end)),
58                peek: true,
59            },
60        ),
61        map(
62            tuple((
63                tag_no_case(b"BODY"),
64                section,
65                opt(delimited(
66                    tag(b"<"),
67                    tuple((number, tag(b"."), nz_number)),
68                    tag(b">"),
69                )),
70            )),
71            |(_, section, byterange)| MessageDataItemName::BodyExt {
72                section,
73                partial: byterange.map(|(start, _, end)| (start, end)),
74                peek: false,
75            },
76        ),
77        value(MessageDataItemName::Body, tag_no_case(b"BODY")),
78        value(MessageDataItemName::Uid, tag_no_case(b"UID")),
79        value(
80            MessageDataItemName::Rfc822Header,
81            tag_no_case(b"RFC822.HEADER"),
82        ),
83        value(MessageDataItemName::Rfc822Size, tag_no_case(b"RFC822.SIZE")),
84        value(MessageDataItemName::Rfc822Text, tag_no_case(b"RFC822.TEXT")),
85        value(MessageDataItemName::Rfc822, tag_no_case(b"RFC822")),
86    ))(input)
87}
88
89/// `msg-att = "("
90///            (msg-att-dynamic / msg-att-static) *(SP (msg-att-dynamic / msg-att-static))
91///            ")"`
92pub(crate) fn msg_att(input: &[u8]) -> IMAPResult<&[u8], NonEmptyVec<MessageDataItem>> {
93    delimited(
94        tag(b"("),
95        map(
96            separated_list1(sp, alt((msg_att_dynamic, msg_att_static))),
97            NonEmptyVec::unvalidated,
98        ),
99        tag(b")"),
100    )(input)
101}
102
103/// `msg-att-dynamic = "FLAGS" SP "(" [flag-fetch *(SP flag-fetch)] ")"`
104///
105/// Note: MAY change for a message
106pub(crate) fn msg_att_dynamic(input: &[u8]) -> IMAPResult<&[u8], MessageDataItem> {
107    let mut parser = tuple((
108        tag_no_case(b"FLAGS"),
109        sp,
110        delimited(tag(b"("), opt(separated_list1(sp, flag_fetch)), tag(b")")),
111    ));
112
113    let (remaining, (_, _, flags)) = parser(input)?;
114
115    Ok((remaining, MessageDataItem::Flags(flags.unwrap_or_default())))
116}
117
118/// `msg-att-static = "ENVELOPE" SP envelope /
119///                   "INTERNALDATE" SP date-time /
120///                   "RFC822" [".HEADER" / ".TEXT"] SP nstring /
121///                   "RFC822.SIZE" SP number /
122///                   "BODY" ["STRUCTURE"] SP body /
123///                   "BODY" section ["<" number ">"] SP nstring /
124///                   "UID" SP uniqueid`
125///
126/// Note: MUST NOT change for a message
127pub(crate) fn msg_att_static(input: &[u8]) -> IMAPResult<&[u8], MessageDataItem> {
128    alt((
129        map(
130            tuple((tag_no_case(b"ENVELOPE"), sp, envelope)),
131            |(_, _, envelope)| MessageDataItem::Envelope(envelope),
132        ),
133        map(
134            tuple((tag_no_case(b"INTERNALDATE"), sp, date_time)),
135            |(_, _, date_time)| MessageDataItem::InternalDate(date_time),
136        ),
137        map(
138            tuple((tag_no_case(b"RFC822.HEADER"), sp, nstring)),
139            |(_, _, nstring)| MessageDataItem::Rfc822Header(nstring),
140        ),
141        map(
142            tuple((tag_no_case(b"RFC822.TEXT"), sp, nstring)),
143            |(_, _, nstring)| MessageDataItem::Rfc822Text(nstring),
144        ),
145        map(
146            tuple((tag_no_case(b"RFC822.SIZE"), sp, number)),
147            |(_, _, num)| MessageDataItem::Rfc822Size(num),
148        ),
149        map(
150            tuple((tag_no_case(b"RFC822"), sp, nstring)),
151            |(_, _, nstring)| MessageDataItem::Rfc822(nstring),
152        ),
153        map(
154            tuple((tag_no_case(b"BODYSTRUCTURE"), sp, body(8))),
155            |(_, _, body)| MessageDataItem::BodyStructure(body),
156        ),
157        map(
158            tuple((tag_no_case(b"BODY"), sp, body(8))),
159            |(_, _, body)| MessageDataItem::Body(body),
160        ),
161        map(
162            tuple((
163                tag_no_case(b"BODY"),
164                section,
165                opt(delimited(tag(b"<"), number, tag(b">"))),
166                sp,
167                nstring,
168            )),
169            |(_, section, origin, _, data)| MessageDataItem::BodyExt {
170                section,
171                origin,
172                data,
173            },
174        ),
175        map(tuple((tag_no_case(b"UID"), sp, uniqueid)), |(_, _, uid)| {
176            MessageDataItem::Uid(uid)
177        }),
178    ))(input)
179}
180
181#[inline]
182/// `uniqueid = nz-number`
183///
184/// Note: Strictly ascending
185pub(crate) fn uniqueid(input: &[u8]) -> IMAPResult<&[u8], NonZeroU32> {
186    nz_number(input)
187}
188
189/// `section = "[" [section-spec] "]"`
190pub(crate) fn section(input: &[u8]) -> IMAPResult<&[u8], Option<Section>> {
191    delimited(tag(b"["), opt(section_spec), tag(b"]"))(input)
192}
193
194/// `section-spec = section-msgtext / (section-part ["." section-text])`
195pub(crate) fn section_spec(input: &[u8]) -> IMAPResult<&[u8], Section> {
196    alt((
197        map(section_msgtext, |part_specifier| match part_specifier {
198            PartSpecifier::PartNumber(_) => unreachable!(),
199            PartSpecifier::Header => Section::Header(None),
200            PartSpecifier::HeaderFields(fields) => Section::HeaderFields(None, fields),
201            PartSpecifier::HeaderFieldsNot(fields) => Section::HeaderFieldsNot(None, fields),
202            PartSpecifier::Text => Section::Text(None),
203            PartSpecifier::Mime => unreachable!(),
204        }),
205        map(
206            tuple((section_part, opt(tuple((tag(b"."), section_text))))),
207            |(part_number, maybe_part_specifier)| {
208                if let Some((_, part_specifier)) = maybe_part_specifier {
209                    match part_specifier {
210                        PartSpecifier::PartNumber(_) => unreachable!(),
211                        PartSpecifier::Header => Section::Header(Some(Part(part_number))),
212                        PartSpecifier::HeaderFields(fields) => {
213                            Section::HeaderFields(Some(Part(part_number)), fields)
214                        }
215                        PartSpecifier::HeaderFieldsNot(fields) => {
216                            Section::HeaderFieldsNot(Some(Part(part_number)), fields)
217                        }
218                        PartSpecifier::Text => Section::Text(Some(Part(part_number))),
219                        PartSpecifier::Mime => Section::Mime(Part(part_number)),
220                    }
221                } else {
222                    Section::Part(Part(part_number))
223                }
224            },
225        ),
226    ))(input)
227}
228
229/// `section-msgtext = "HEADER" / "HEADER.FIELDS" [".NOT"] SP header-list / "TEXT"`
230///
231/// Top-level or MESSAGE/RFC822 part
232pub(crate) fn section_msgtext(input: &[u8]) -> IMAPResult<&[u8], PartSpecifier> {
233    alt((
234        map(
235            tuple((tag_no_case(b"HEADER.FIELDS.NOT"), sp, header_list)),
236            |(_, _, header_list)| PartSpecifier::HeaderFieldsNot(header_list),
237        ),
238        map(
239            tuple((tag_no_case(b"HEADER.FIELDS"), sp, header_list)),
240            |(_, _, header_list)| PartSpecifier::HeaderFields(header_list),
241        ),
242        value(PartSpecifier::Header, tag_no_case(b"HEADER")),
243        value(PartSpecifier::Text, tag_no_case(b"TEXT")),
244    ))(input)
245}
246
247#[inline]
248/// `section-part = nz-number *("." nz-number)`
249///
250/// Body part nesting
251pub(crate) fn section_part(input: &[u8]) -> IMAPResult<&[u8], NonEmptyVec<NonZeroU32>> {
252    map(
253        separated_list1(tag(b"."), nz_number),
254        NonEmptyVec::unvalidated,
255    )(input)
256}
257
258/// `section-text = section-msgtext / "MIME"`
259///
260/// Text other than actual body part (headers, etc.)
261pub(crate) fn section_text(input: &[u8]) -> IMAPResult<&[u8], PartSpecifier> {
262    alt((
263        section_msgtext,
264        value(PartSpecifier::Mime, tag_no_case(b"MIME")),
265    ))(input)
266}
267
268/// `header-list = "(" header-fld-name *(SP header-fld-name) ")"`
269pub(crate) fn header_list(input: &[u8]) -> IMAPResult<&[u8], NonEmptyVec<AString>> {
270    map(
271        delimited(tag(b"("), separated_list1(sp, header_fld_name), tag(b")")),
272        NonEmptyVec::unvalidated,
273    )(input)
274}
275
276#[inline]
277/// `header-fld-name = astring`
278pub(crate) fn header_fld_name(input: &[u8]) -> IMAPResult<&[u8], AString> {
279    astring(input)
280}
281
282#[cfg(test)]
283mod tests {
284    use imap_types::{
285        body::{BasicFields, Body, BodyStructure, SpecificFields},
286        core::{IString, NString},
287        datetime::DateTime,
288        envelope::Envelope,
289    };
290
291    use super::*;
292    use crate::testing::known_answer_test_encode;
293
294    #[test]
295    fn test_encode_message_data_item_name() {
296        let tests = [
297            (MessageDataItemName::Body, b"BODY".as_ref()),
298            (
299                MessageDataItemName::BodyExt {
300                    section: None,
301                    partial: None,
302                    peek: false,
303                },
304                b"BODY[]",
305            ),
306            (MessageDataItemName::BodyStructure, b"BODYSTRUCTURE"),
307            (MessageDataItemName::Envelope, b"ENVELOPE"),
308            (MessageDataItemName::Flags, b"FLAGS"),
309            (MessageDataItemName::InternalDate, b"INTERNALDATE"),
310            (MessageDataItemName::Rfc822, b"RFC822"),
311            (MessageDataItemName::Rfc822Header, b"RFC822.HEADER"),
312            (MessageDataItemName::Rfc822Size, b"RFC822.SIZE"),
313            (MessageDataItemName::Rfc822Text, b"RFC822.TEXT"),
314            (MessageDataItemName::Uid, b"UID"),
315        ];
316
317        for test in tests {
318            known_answer_test_encode(test);
319        }
320    }
321
322    #[test]
323    fn test_encode_message_data_item() {
324        let tests = [
325            (
326                MessageDataItem::Body(BodyStructure::Single {
327                    body: Body {
328                        basic: BasicFields {
329                            parameter_list: vec![],
330                            id: NString(None),
331                            description: NString(None),
332                            content_transfer_encoding: IString::try_from("base64").unwrap(),
333                            size: 42,
334                        },
335                        specific: SpecificFields::Text {
336                            subtype: IString::try_from("foo").unwrap(),
337                            number_of_lines: 1337,
338                        },
339                    },
340                    extension_data: None,
341                }),
342                b"BODY (\"TEXT\" \"foo\" NIL NIL NIL \"base64\" 42 1337)".as_ref(),
343            ),
344            (
345                MessageDataItem::BodyExt {
346                    section: None,
347                    origin: None,
348                    data: NString(None),
349                },
350                b"BODY[] NIL",
351            ),
352            (
353                MessageDataItem::BodyExt {
354                    section: None,
355                    origin: Some(123),
356                    data: NString(None),
357                },
358                b"BODY[]<123> NIL",
359            ),
360            (
361                MessageDataItem::BodyStructure(BodyStructure::Single {
362                    body: Body {
363                        basic: BasicFields {
364                            parameter_list: vec![],
365                            id: NString(None),
366                            description: NString(None),
367                            content_transfer_encoding: IString::try_from("base64").unwrap(),
368                            size: 213,
369                        },
370                        specific: SpecificFields::Text {
371                            subtype: IString::try_from("").unwrap(),
372                            number_of_lines: 224,
373                        },
374                    },
375                    extension_data: None,
376                }),
377                b"BODYSTRUCTURE (\"TEXT\" \"\" NIL NIL NIL \"base64\" 213 224)",
378            ),
379            (
380                MessageDataItem::Envelope(Envelope {
381                    date: NString(None),
382                    subject: NString(None),
383                    from: vec![],
384                    sender: vec![],
385                    reply_to: vec![],
386                    to: vec![],
387                    cc: vec![],
388                    bcc: vec![],
389                    in_reply_to: NString(None),
390                    message_id: NString(None),
391                }),
392                b"ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)",
393            ),
394            (MessageDataItem::Flags(vec![]), b"FLAGS ()"),
395            (
396                MessageDataItem::InternalDate(
397                    DateTime::try_from(
398                        chrono::DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200")
399                            .unwrap(),
400                    )
401                    .unwrap(),
402                ),
403                b"INTERNALDATE \"01-Jul-2003 10:52:37 +0200\"",
404            ),
405            (MessageDataItem::Rfc822(NString(None)), b"RFC822 NIL"),
406            (
407                MessageDataItem::Rfc822Header(NString(None)),
408                b"RFC822.HEADER NIL",
409            ),
410            (MessageDataItem::Rfc822Size(3456), b"RFC822.SIZE 3456"),
411            (
412                MessageDataItem::Rfc822Text(NString(None)),
413                b"RFC822.TEXT NIL",
414            ),
415            (
416                MessageDataItem::Uid(NonZeroU32::try_from(u32::MAX).unwrap()),
417                b"UID 4294967295",
418            ),
419        ];
420
421        for test in tests {
422            known_answer_test_encode(test);
423        }
424    }
425
426    #[test]
427    fn test_encode_section() {
428        let tests = [
429            (
430                Section::Part(Part(NonEmptyVec::from(NonZeroU32::try_from(1).unwrap()))),
431                b"1".as_ref(),
432            ),
433            (Section::Header(None), b"HEADER"),
434            (
435                Section::Header(Some(Part(NonEmptyVec::from(
436                    NonZeroU32::try_from(1).unwrap(),
437                )))),
438                b"1.HEADER",
439            ),
440            (
441                Section::HeaderFields(None, NonEmptyVec::from(AString::try_from("").unwrap())),
442                b"HEADER.FIELDS (\"\")",
443            ),
444            (
445                Section::HeaderFields(
446                    Some(Part(NonEmptyVec::from(NonZeroU32::try_from(1).unwrap()))),
447                    NonEmptyVec::from(AString::try_from("").unwrap()),
448                ),
449                b"1.HEADER.FIELDS (\"\")",
450            ),
451            (
452                Section::HeaderFieldsNot(None, NonEmptyVec::from(AString::try_from("").unwrap())),
453                b"HEADER.FIELDS.NOT (\"\")",
454            ),
455            (
456                Section::HeaderFieldsNot(
457                    Some(Part(NonEmptyVec::from(NonZeroU32::try_from(1).unwrap()))),
458                    NonEmptyVec::from(AString::try_from("").unwrap()),
459                ),
460                b"1.HEADER.FIELDS.NOT (\"\")",
461            ),
462            (Section::Text(None), b"TEXT"),
463            (
464                Section::Text(Some(Part(NonEmptyVec::from(
465                    NonZeroU32::try_from(1).unwrap(),
466                )))),
467                b"1.TEXT",
468            ),
469            (
470                Section::Mime(Part(NonEmptyVec::from(NonZeroU32::try_from(1).unwrap()))),
471                b"1.MIME",
472            ),
473        ];
474
475        for test in tests {
476            known_answer_test_encode(test)
477        }
478    }
479}