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