imap_proto/parser/rfc3501/
mod.rs

1//!
2//! https://tools.ietf.org/html/rfc3501
3//!
4//! INTERNET MESSAGE ACCESS PROTOCOL
5//!
6
7use std::borrow::Cow;
8use std::str::from_utf8;
9
10use nom::{
11    branch::alt,
12    bytes::streaming::{tag, tag_no_case, take_while, take_while1},
13    character::streaming::char,
14    combinator::{map, map_res, opt, recognize, value},
15    multi::{many0, many1},
16    sequence::{delimited, pair, preceded, terminated, tuple},
17    IResult,
18};
19
20use crate::{
21    parser::{
22        core::*, rfc2087, rfc2971, rfc3501::body::*, rfc3501::body_structure::*, rfc4314, rfc4315,
23        rfc4551, rfc5161, rfc5256, rfc5464, rfc7162,
24    },
25    types::*,
26};
27
28use super::gmail;
29
30pub mod body;
31pub mod body_structure;
32
33fn is_tag_char(c: u8) -> bool {
34    c != b'+' && is_astring_char(c)
35}
36
37fn status_ok(i: &[u8]) -> IResult<&[u8], Status> {
38    map(tag_no_case("OK"), |_s| Status::Ok)(i)
39}
40fn status_no(i: &[u8]) -> IResult<&[u8], Status> {
41    map(tag_no_case("NO"), |_s| Status::No)(i)
42}
43fn status_bad(i: &[u8]) -> IResult<&[u8], Status> {
44    map(tag_no_case("BAD"), |_s| Status::Bad)(i)
45}
46fn status_preauth(i: &[u8]) -> IResult<&[u8], Status> {
47    map(tag_no_case("PREAUTH"), |_s| Status::PreAuth)(i)
48}
49fn status_bye(i: &[u8]) -> IResult<&[u8], Status> {
50    map(tag_no_case("BYE"), |_s| Status::Bye)(i)
51}
52
53fn status(i: &[u8]) -> IResult<&[u8], Status> {
54    alt((status_ok, status_no, status_bad, status_preauth, status_bye))(i)
55}
56
57pub(crate) fn mailbox(i: &[u8]) -> IResult<&[u8], &str> {
58    map(astring_utf8, |s| {
59        if s.eq_ignore_ascii_case("INBOX") {
60            "INBOX"
61        } else {
62            s
63        }
64    })(i)
65}
66
67fn flag_extension(i: &[u8]) -> IResult<&[u8], &str> {
68    map_res(
69        recognize(pair(tag(b"\\"), take_while(is_atom_char))),
70        from_utf8,
71    )(i)
72}
73
74pub(crate) fn flag(i: &[u8]) -> IResult<&[u8], &str> {
75    // Correct code is
76    //   alt((flag_extension, atom))(i)
77    //
78    // Unfortunately, some unknown providers send the following response:
79    // * FLAGS (OIB-Seen-[Gmail]/All)
80    //
81    // As a workaround, ']' (resp-specials) is allowed here.
82    alt((
83        flag_extension,
84        map_res(take_while1(is_astring_char), from_utf8),
85    ))(i)
86}
87
88fn flag_list(i: &[u8]) -> IResult<&[u8], Vec<Cow<str>>> {
89    // Correct code is
90    //   parenthesized_list(flag)(i)
91    //
92    // Unfortunately, Zoho Mail Server (imap.zoho.com) sends the following response:
93    // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)
94    //
95    // As a workaround, "\*" is allowed here.
96    parenthesized_list(map(flag_perm, Cow::Borrowed))(i)
97}
98
99fn flag_perm(i: &[u8]) -> IResult<&[u8], &str> {
100    alt((map_res(tag(b"\\*"), from_utf8), flag))(i)
101}
102
103fn resp_text_code_alert(i: &[u8]) -> IResult<&[u8], ResponseCode> {
104    map(tag_no_case(b"ALERT"), |_| ResponseCode::Alert)(i)
105}
106
107fn resp_text_code_badcharset(i: &[u8]) -> IResult<&[u8], ResponseCode> {
108    map(
109        preceded(
110            tag_no_case(b"BADCHARSET"),
111            opt(preceded(
112                tag(b" "),
113                parenthesized_nonempty_list(map(astring_utf8, Cow::Borrowed)),
114            )),
115        ),
116        ResponseCode::BadCharset,
117    )(i)
118}
119
120fn resp_text_code_capability(i: &[u8]) -> IResult<&[u8], ResponseCode> {
121    map(capability_data, ResponseCode::Capabilities)(i)
122}
123
124fn resp_text_code_parse(i: &[u8]) -> IResult<&[u8], ResponseCode> {
125    map(tag_no_case(b"PARSE"), |_| ResponseCode::Parse)(i)
126}
127
128fn resp_text_code_permanent_flags(i: &[u8]) -> IResult<&[u8], ResponseCode> {
129    map(
130        preceded(
131            tag_no_case(b"PERMANENTFLAGS "),
132            parenthesized_list(map(flag_perm, Cow::Borrowed)),
133        ),
134        ResponseCode::PermanentFlags,
135    )(i)
136}
137
138fn resp_text_code_read_only(i: &[u8]) -> IResult<&[u8], ResponseCode> {
139    map(tag_no_case(b"READ-ONLY"), |_| ResponseCode::ReadOnly)(i)
140}
141
142fn resp_text_code_read_write(i: &[u8]) -> IResult<&[u8], ResponseCode> {
143    map(tag_no_case(b"READ-WRITE"), |_| ResponseCode::ReadWrite)(i)
144}
145
146fn resp_text_code_try_create(i: &[u8]) -> IResult<&[u8], ResponseCode> {
147    map(tag_no_case(b"TRYCREATE"), |_| ResponseCode::TryCreate)(i)
148}
149
150fn resp_text_code_uid_validity(i: &[u8]) -> IResult<&[u8], ResponseCode> {
151    map(
152        preceded(tag_no_case(b"UIDVALIDITY "), number),
153        ResponseCode::UidValidity,
154    )(i)
155}
156
157fn resp_text_code_uid_next(i: &[u8]) -> IResult<&[u8], ResponseCode> {
158    map(
159        preceded(tag_no_case(b"UIDNEXT "), number),
160        ResponseCode::UidNext,
161    )(i)
162}
163
164fn resp_text_code_unseen(i: &[u8]) -> IResult<&[u8], ResponseCode> {
165    map(
166        preceded(tag_no_case(b"UNSEEN "), number),
167        ResponseCode::Unseen,
168    )(i)
169}
170
171fn resp_text_code(i: &[u8]) -> IResult<&[u8], ResponseCode> {
172    // Per the spec, the closing tag should be "] ".
173    // See `resp_text` for more on why this is done differently.
174    delimited(
175        tag(b"["),
176        alt((
177            resp_text_code_alert,
178            resp_text_code_badcharset,
179            resp_text_code_capability,
180            resp_text_code_parse,
181            resp_text_code_permanent_flags,
182            resp_text_code_uid_validity,
183            resp_text_code_uid_next,
184            resp_text_code_unseen,
185            resp_text_code_read_only,
186            resp_text_code_read_write,
187            resp_text_code_try_create,
188            rfc4551::resp_text_code_highest_mod_seq,
189            rfc4315::resp_text_code_append_uid,
190            rfc4315::resp_text_code_copy_uid,
191            rfc4315::resp_text_code_uid_not_sticky,
192            rfc5464::resp_text_code_metadata_long_entries,
193            rfc5464::resp_text_code_metadata_max_size,
194            rfc5464::resp_text_code_metadata_too_many,
195            rfc5464::resp_text_code_metadata_no_private,
196        )),
197        tag(b"]"),
198    )(i)
199}
200
201fn capability(i: &[u8]) -> IResult<&[u8], Capability> {
202    alt((
203        map(tag_no_case(b"IMAP4rev1"), |_| Capability::Imap4rev1),
204        map(
205            map(preceded(tag_no_case(b"AUTH="), atom), Cow::Borrowed),
206            Capability::Auth,
207        ),
208        map(map(atom, Cow::Borrowed), Capability::Atom),
209    ))(i)
210}
211
212fn ensure_capabilities_contains_imap4rev(
213    capabilities: Vec<Capability<'_>>,
214) -> Result<Vec<Capability<'_>>, ()> {
215    if capabilities.contains(&Capability::Imap4rev1) {
216        Ok(capabilities)
217    } else {
218        Err(())
219    }
220}
221
222fn capability_data(i: &[u8]) -> IResult<&[u8], Vec<Capability>> {
223    map_res(
224        preceded(
225            tag_no_case(b"CAPABILITY"),
226            many0(preceded(char(' '), capability)),
227        ),
228        ensure_capabilities_contains_imap4rev,
229    )(i)
230}
231
232fn mailbox_data_search(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
233    map(
234        // Technically, trailing whitespace is not allowed here, but multiple
235        // email servers in the wild seem to have it anyway (see #34, #108).
236        terminated(
237            preceded(tag_no_case(b"SEARCH"), many0(preceded(tag(" "), number))),
238            opt(tag(" ")),
239        ),
240        MailboxDatum::Search,
241    )(i)
242}
243
244fn mailbox_data_flags(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
245    map(
246        preceded(tag_no_case("FLAGS "), flag_list),
247        MailboxDatum::Flags,
248    )(i)
249}
250
251fn mailbox_data_exists(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
252    map(
253        terminated(number, tag_no_case(" EXISTS")),
254        MailboxDatum::Exists,
255    )(i)
256}
257
258fn name_attribute(i: &[u8]) -> IResult<&[u8], NameAttribute> {
259    alt((
260        // RFC 3501
261        value(NameAttribute::NoInferiors, tag_no_case(b"\\Noinferiors")),
262        value(NameAttribute::NoSelect, tag_no_case(b"\\Noselect")),
263        value(NameAttribute::Marked, tag_no_case(b"\\Marked")),
264        value(NameAttribute::Unmarked, tag_no_case(b"\\Unmarked")),
265        // RFC 6154
266        value(NameAttribute::All, tag_no_case(b"\\All")),
267        value(NameAttribute::Archive, tag_no_case(b"\\Archive")),
268        value(NameAttribute::Drafts, tag_no_case(b"\\Drafts")),
269        value(NameAttribute::Flagged, tag_no_case(b"\\Flagged")),
270        value(NameAttribute::Junk, tag_no_case(b"\\Junk")),
271        value(NameAttribute::Sent, tag_no_case(b"\\Sent")),
272        value(NameAttribute::Trash, tag_no_case(b"\\Trash")),
273        // Extensions not supported by this crate
274        map(
275            map_res(
276                recognize(pair(tag(b"\\"), take_while(is_atom_char))),
277                from_utf8,
278            ),
279            |s| NameAttribute::Extension(Cow::Borrowed(s)),
280        ),
281    ))(i)
282}
283
284#[allow(clippy::type_complexity)]
285fn mailbox_list(i: &[u8]) -> IResult<&[u8], (Vec<NameAttribute>, Option<&str>, &str)> {
286    map(
287        tuple((
288            parenthesized_list(name_attribute),
289            tag(b" "),
290            alt((map(quoted_utf8, Some), map(nil, |_| None))),
291            tag(b" "),
292            mailbox,
293        )),
294        |(name_attributes, _, delimiter, _, name)| (name_attributes, delimiter, name),
295    )(i)
296}
297
298fn mailbox_data_list(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
299    map(preceded(tag_no_case("LIST "), mailbox_list), |data| {
300        MailboxDatum::List {
301            name_attributes: data.0,
302            delimiter: data.1.map(Cow::Borrowed),
303            name: Cow::Borrowed(data.2),
304        }
305    })(i)
306}
307
308fn mailbox_data_lsub(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
309    map(preceded(tag_no_case("LSUB "), mailbox_list), |data| {
310        MailboxDatum::List {
311            name_attributes: data.0,
312            delimiter: data.1.map(Cow::Borrowed),
313            name: Cow::Borrowed(data.2),
314        }
315    })(i)
316}
317
318// Unlike `status_att` in the RFC syntax, this includes the value,
319// so that it can return a valid enum object instead of just a key.
320fn status_att(i: &[u8]) -> IResult<&[u8], StatusAttribute> {
321    alt((
322        rfc4551::status_att_val_highest_mod_seq,
323        map(
324            preceded(tag_no_case("MESSAGES "), number),
325            StatusAttribute::Messages,
326        ),
327        map(
328            preceded(tag_no_case("RECENT "), number),
329            StatusAttribute::Recent,
330        ),
331        map(
332            preceded(tag_no_case("UIDNEXT "), number),
333            StatusAttribute::UidNext,
334        ),
335        map(
336            preceded(tag_no_case("UIDVALIDITY "), number),
337            StatusAttribute::UidValidity,
338        ),
339        map(
340            preceded(tag_no_case("UNSEEN "), number),
341            StatusAttribute::Unseen,
342        ),
343    ))(i)
344}
345
346fn status_att_list(i: &[u8]) -> IResult<&[u8], Vec<StatusAttribute>> {
347    // RFC 3501 specifies that the list is non-empty in the formal grammar
348    //   status-att-list =  status-att SP number *(SP status-att SP number)
349    // but mail.163.com sends an empty list in STATUS response anyway.
350    parenthesized_list(status_att)(i)
351}
352
353fn mailbox_data_status(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
354    map(
355        tuple((tag_no_case("STATUS "), mailbox, tag(" "), status_att_list)),
356        |(_, mailbox, _, status)| MailboxDatum::Status {
357            mailbox: Cow::Borrowed(mailbox),
358            status,
359        },
360    )(i)
361}
362
363fn mailbox_data_recent(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
364    map(
365        terminated(number, tag_no_case(" RECENT")),
366        MailboxDatum::Recent,
367    )(i)
368}
369
370fn mailbox_data(i: &[u8]) -> IResult<&[u8], MailboxDatum> {
371    alt((
372        mailbox_data_flags,
373        mailbox_data_exists,
374        mailbox_data_list,
375        mailbox_data_lsub,
376        mailbox_data_status,
377        mailbox_data_recent,
378        mailbox_data_search,
379        gmail::mailbox_data_gmail_labels,
380        gmail::mailbox_data_gmail_msgid,
381        rfc5256::mailbox_data_sort,
382    ))(i)
383}
384
385// An address structure is a parenthesized list that describes an
386// electronic mail address.
387fn address(i: &[u8]) -> IResult<&[u8], Address> {
388    paren_delimited(map(
389        tuple((
390            nstring,
391            tag(" "),
392            nstring,
393            tag(" "),
394            nstring,
395            tag(" "),
396            nstring,
397        )),
398        |(name, _, adl, _, mailbox, _, host)| Address {
399            name: name.map(Cow::Borrowed),
400            adl: adl.map(Cow::Borrowed),
401            mailbox: mailbox.map(Cow::Borrowed),
402            host: host.map(Cow::Borrowed),
403        },
404    ))(i)
405}
406
407fn opt_addresses(i: &[u8]) -> IResult<&[u8], Option<Vec<Address>>> {
408    alt((
409        map(nil, |_s| None),
410        map(
411            paren_delimited(many1(terminated(address, opt(char(' '))))),
412            Some,
413        ),
414    ))(i)
415}
416
417// envelope        = "(" env-date SP env-subject SP env-from SP
418//                   env-sender SP env-reply-to SP env-to SP env-cc SP
419//                   env-bcc SP env-in-reply-to SP env-message-id ")"
420//
421// env-bcc         = "(" 1*address ")" / nil
422//
423// env-cc          = "(" 1*address ")" / nil
424//
425// env-date        = nstring
426//
427// env-from        = "(" 1*address ")" / nil
428//
429// env-in-reply-to = nstring
430//
431// env-message-id  = nstring
432//
433// env-reply-to    = "(" 1*address ")" / nil
434//
435// env-sender      = "(" 1*address ")" / nil
436//
437// env-subject     = nstring
438//
439// env-to          = "(" 1*address ")" / nil
440pub(crate) fn envelope(i: &[u8]) -> IResult<&[u8], Envelope> {
441    paren_delimited(map(
442        tuple((
443            nstring,
444            tag(" "),
445            nstring,
446            tag(" "),
447            opt_addresses,
448            tag(" "),
449            opt_addresses,
450            tag(" "),
451            opt_addresses,
452            tag(" "),
453            opt_addresses,
454            tag(" "),
455            opt_addresses,
456            tag(" "),
457            opt_addresses,
458            tag(" "),
459            nstring,
460            tag(" "),
461            nstring,
462        )),
463        |(
464            date,
465            _,
466            subject,
467            _,
468            from,
469            _,
470            sender,
471            _,
472            reply_to,
473            _,
474            to,
475            _,
476            cc,
477            _,
478            bcc,
479            _,
480            in_reply_to,
481            _,
482            message_id,
483        )| Envelope {
484            date: date.map(Cow::Borrowed),
485            subject: subject.map(Cow::Borrowed),
486            from,
487            sender,
488            reply_to,
489            to,
490            cc,
491            bcc,
492            in_reply_to: in_reply_to.map(Cow::Borrowed),
493            message_id: message_id.map(Cow::Borrowed),
494        },
495    ))(i)
496}
497
498fn msg_att_envelope(i: &[u8]) -> IResult<&[u8], AttributeValue> {
499    map(preceded(tag_no_case("ENVELOPE "), envelope), |envelope| {
500        AttributeValue::Envelope(Box::new(envelope))
501    })(i)
502}
503
504fn msg_att_internal_date(i: &[u8]) -> IResult<&[u8], AttributeValue> {
505    map(
506        preceded(tag_no_case("INTERNALDATE "), nstring_utf8),
507        |date| AttributeValue::InternalDate(Cow::Borrowed(date.unwrap())),
508    )(i)
509}
510
511fn msg_att_flags(i: &[u8]) -> IResult<&[u8], AttributeValue> {
512    map(
513        preceded(tag_no_case("FLAGS "), flag_list),
514        AttributeValue::Flags,
515    )(i)
516}
517
518fn msg_att_rfc822(i: &[u8]) -> IResult<&[u8], AttributeValue> {
519    map(preceded(tag_no_case("RFC822 "), nstring), |v| {
520        AttributeValue::Rfc822(v.map(Cow::Borrowed))
521    })(i)
522}
523
524fn msg_att_rfc822_header(i: &[u8]) -> IResult<&[u8], AttributeValue> {
525    // extra space workaround for DavMail
526    map(
527        tuple((tag_no_case("RFC822.HEADER "), opt(tag(b" ")), nstring)),
528        |(_, _, raw)| AttributeValue::Rfc822Header(raw.map(Cow::Borrowed)),
529    )(i)
530}
531
532fn msg_att_rfc822_size(i: &[u8]) -> IResult<&[u8], AttributeValue> {
533    map(
534        preceded(tag_no_case("RFC822.SIZE "), number),
535        AttributeValue::Rfc822Size,
536    )(i)
537}
538
539fn msg_att_rfc822_text(i: &[u8]) -> IResult<&[u8], AttributeValue> {
540    map(preceded(tag_no_case("RFC822.TEXT "), nstring), |v| {
541        AttributeValue::Rfc822Text(v.map(Cow::Borrowed))
542    })(i)
543}
544
545fn msg_att_uid(i: &[u8]) -> IResult<&[u8], AttributeValue> {
546    map(preceded(tag_no_case("UID "), number), AttributeValue::Uid)(i)
547}
548
549// msg-att         = "(" (msg-att-dynamic / msg-att-static)
550//                    *(SP (msg-att-dynamic / msg-att-static)) ")"
551//
552// msg-att-dynamic = "FLAGS" SP "(" [flag-fetch *(SP flag-fetch)] ")"
553//                     ; MAY change for a message
554//
555// msg-att-static  = "ENVELOPE" SP envelope / "INTERNALDATE" SP date-time /
556//                   "RFC822" [".HEADER" / ".TEXT"] SP nstring /
557//                   "RFC822.SIZE" SP number /
558//                   "BODY" ["STRUCTURE"] SP body /
559//                   "BODY" section ["<" number ">"] SP nstring /
560//                   "UID" SP uniqueid
561//                     ; MUST NOT change for a message
562fn msg_att(i: &[u8]) -> IResult<&[u8], AttributeValue> {
563    alt((
564        msg_att_body_section,
565        msg_att_body_structure,
566        msg_att_envelope,
567        msg_att_internal_date,
568        msg_att_flags,
569        rfc4551::msg_att_mod_seq,
570        msg_att_rfc822,
571        msg_att_rfc822_header,
572        msg_att_rfc822_size,
573        msg_att_rfc822_text,
574        msg_att_uid,
575        gmail::msg_att_gmail_labels,
576        gmail::msg_att_gmail_msgid,
577    ))(i)
578}
579
580fn msg_att_list(i: &[u8]) -> IResult<&[u8], Vec<AttributeValue>> {
581    parenthesized_nonempty_list(msg_att)(i)
582}
583
584// message-data    = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
585fn message_data_fetch(i: &[u8]) -> IResult<&[u8], Response> {
586    map(
587        tuple((number, tag_no_case(" FETCH "), msg_att_list)),
588        |(num, _, attrs)| Response::Fetch(num, attrs),
589    )(i)
590}
591
592// message-data    = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
593fn message_data_expunge(i: &[u8]) -> IResult<&[u8], u32> {
594    terminated(number, tag_no_case(" EXPUNGE"))(i)
595}
596
597// tag             = 1*<any ASTRING-CHAR except "+">
598fn imap_tag(i: &[u8]) -> IResult<&[u8], RequestId> {
599    map(map_res(take_while1(is_tag_char), from_utf8), |s| {
600        RequestId(s.to_string())
601    })(i)
602}
603
604// This is not quite according to spec, which mandates the following:
605//     ["[" resp-text-code "]" SP] text
606// However, examples in RFC 4551 (Conditional STORE) counteract this by giving
607// examples of `resp-text` that do not include the trailing space and text.
608fn resp_text(i: &[u8]) -> IResult<&[u8], (Option<ResponseCode>, Option<&str>)> {
609    map(tuple((opt(resp_text_code), text)), |(code, text)| {
610        let res = if text.is_empty() {
611            None
612        } else if code.is_some() {
613            Some(&text[1..])
614        } else {
615            Some(text)
616        };
617        (code, res)
618    })(i)
619}
620
621// an response-text if it is at the end of a response. Empty text is then allowed without the normally needed trailing space.
622fn trailing_resp_text(i: &[u8]) -> IResult<&[u8], (Option<ResponseCode>, Option<&str>)> {
623    map(opt(tuple((tag(b" "), resp_text))), |resptext| {
624        resptext.map(|(_, tuple)| tuple).unwrap_or((None, None))
625    })(i)
626}
627
628// continue-req    = "+" SP (resp-text / base64) CRLF
629pub(crate) fn continue_req(i: &[u8]) -> IResult<&[u8], Response> {
630    // Some servers do not send the space :/
631    // TODO: base64
632    map(
633        tuple((tag("+"), opt(tag(" ")), resp_text, tag("\r\n"))),
634        |(_, _, text, _)| Response::Continue {
635            code: text.0,
636            information: text.1.map(Cow::Borrowed),
637        },
638    )(i)
639}
640
641// response-tagged = tag SP resp-cond-state CRLF
642//
643// resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
644//                     ; Status condition
645pub(crate) fn response_tagged(i: &[u8]) -> IResult<&[u8], Response> {
646    map(
647        tuple((
648            imap_tag,
649            tag(b" "),
650            status,
651            trailing_resp_text,
652            tag(b"\r\n"),
653        )),
654        |(tag, _, status, text, _)| Response::Done {
655            tag,
656            status,
657            code: text.0,
658            information: text.1.map(Cow::Borrowed),
659        },
660    )(i)
661}
662
663// resp-cond-auth  = ("OK" / "PREAUTH") SP resp-text
664//                     ; Authentication condition
665//
666// resp-cond-bye   = "BYE" SP resp-text
667//
668// resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
669//                     ; Status condition
670fn resp_cond(i: &[u8]) -> IResult<&[u8], Response> {
671    map(tuple((status, trailing_resp_text)), |(status, text)| {
672        Response::Data {
673            status,
674            code: text.0,
675            information: text.1.map(Cow::Borrowed),
676        }
677    })(i)
678}
679
680// response-data   = "*" SP (resp-cond-state / resp-cond-bye /
681//                   mailbox-data / message-data / capability-data / quota) CRLF
682pub(crate) fn response_data(i: &[u8]) -> IResult<&[u8], Response> {
683    delimited(
684        tag(b"* "),
685        alt((
686            resp_cond,
687            map(mailbox_data, Response::MailboxData),
688            map(message_data_expunge, Response::Expunge),
689            message_data_fetch,
690            map(capability_data, Response::Capabilities),
691            rfc5161::resp_enabled,
692            rfc5464::metadata_solicited,
693            rfc5464::metadata_unsolicited,
694            rfc7162::resp_vanished,
695            rfc2087::quota,
696            rfc2087::quota_root,
697            rfc2971::resp_id,
698            rfc4314::acl,
699            rfc4314::list_rights,
700            rfc4314::my_rights,
701        )),
702        preceded(
703            many0(tag(b" ")), // Outlook server sometimes sends whitespace at the end of STATUS response.
704            tag(b"\r\n"),
705        ),
706    )(i)
707}
708
709#[cfg(test)]
710mod tests {
711    use crate::types::*;
712    use assert_matches::assert_matches;
713    use std::borrow::Cow;
714
715    #[test]
716    fn test_list() {
717        match super::mailbox(b"iNboX ") {
718            Ok((_, mb)) => {
719                assert_eq!(mb, "INBOX");
720            }
721            rsp => panic!("unexpected response {rsp:?}"),
722        }
723    }
724
725    #[test]
726    fn test_envelope() {
727        let env = br#"ENVELOPE ("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)" "IMAP4rev1 WG mtg summary and minutes" (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) (("Terry Gray" NIL "gray" "cac.washington.edu")) ((NIL NIL "imap" "cac.washington.edu")) ((NIL NIL "minutes" "CNRI.Reston.VA.US") ("John Klensin" NIL "KLENSIN" "MIT.EDU")) NIL NIL "<B27397-0100000@cac.washington.edu>") "#;
728        match super::msg_att_envelope(env) {
729            Ok((_, AttributeValue::Envelope(_))) => {}
730            rsp => panic!("unexpected response {rsp:?}"),
731        }
732    }
733
734    #[test]
735    fn test_opt_addresses() {
736        let addr = b"((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") (\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) ";
737        match super::opt_addresses(addr) {
738            Ok((_, _addresses)) => {}
739            rsp => panic!("unexpected response {rsp:?}"),
740        }
741    }
742
743    #[test]
744    fn test_opt_addresses_no_space() {
745        let addr =
746            br#"((NIL NIL "test" "example@example.com")(NIL NIL "test" "example@example.com"))"#;
747        match super::opt_addresses(addr) {
748            Ok((_, _addresses)) => {}
749            rsp => panic!("unexpected response {rsp:?}"),
750        }
751    }
752
753    #[test]
754    fn test_addresses() {
755        match super::address(b"(\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\") ") {
756            Ok((_, _address)) => {}
757            rsp => panic!("unexpected response {rsp:?}"),
758        }
759
760        // Literal non-UTF8 address
761        match super::address(b"({12}\r\nJoh\xff Klensin NIL \"KLENSIN\" \"MIT.EDU\") ") {
762            Ok((_, _address)) => {}
763            rsp => panic!("unexpected response {rsp:?}"),
764        }
765    }
766
767    #[test]
768    fn test_capability_data() {
769        // Minimal capabilities
770        assert_matches!(
771            super::capability_data(b"CAPABILITY IMAP4rev1\r\n"),
772            Ok((_, capabilities)) => {
773                assert_eq!(capabilities, vec![Capability::Imap4rev1])
774            }
775        );
776
777        assert_matches!(
778            super::capability_data(b"CAPABILITY XPIG-LATIN IMAP4rev1 STARTTLS AUTH=GSSAPI\r\n"),
779            Ok((_, capabilities)) => {
780                assert_eq!(capabilities, vec![
781                    Capability::Atom(Cow::Borrowed("XPIG-LATIN")),
782                    Capability::Imap4rev1,
783                    Capability::Atom(Cow::Borrowed("STARTTLS")),
784                    Capability::Auth(Cow::Borrowed("GSSAPI")),
785                ])
786            }
787        );
788
789        assert_matches!(
790            super::capability_data(b"CAPABILITY IMAP4rev1 AUTH=GSSAPI AUTH=PLAIN\r\n"),
791            Ok((_, capabilities)) => {
792                assert_eq!(capabilities, vec![
793                    Capability::Imap4rev1,
794                    Capability::Auth(Cow::Borrowed("GSSAPI")),
795                    Capability::Auth(Cow::Borrowed("PLAIN")),
796                ])
797            }
798        );
799
800        // Capability command must contain IMAP4rev1
801        assert_matches!(
802            super::capability_data(b"CAPABILITY AUTH=GSSAPI AUTH=PLAIN\r\n"),
803            Err(_)
804        );
805    }
806}