Skip to main content

imap_proto/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},
17    IResult, Parser,
18};
19
20use crate::{
21    core::*, rfc2087, rfc2971, rfc3501::body::*, rfc3501::body_structure::*, rfc4314, rfc4315,
22    rfc4315::UidSetMember, rfc4551, rfc5161, rfc5256, rfc5464, rfc5464::Metadata, rfc7162,
23    Response,
24};
25
26use super::gmail;
27
28pub mod body;
29pub mod body_structure;
30
31fn is_tag_char(c: u8) -> bool {
32    c != b'+' && is_astring_char(c)
33}
34
35fn status_ok(i: &[u8]) -> IResult<&[u8], Status> {
36    map(tag_no_case("OK"), |_s| Status::Ok).parse(i)
37}
38fn status_no(i: &[u8]) -> IResult<&[u8], Status> {
39    map(tag_no_case("NO"), |_s| Status::No).parse(i)
40}
41fn status_bad(i: &[u8]) -> IResult<&[u8], Status> {
42    map(tag_no_case("BAD"), |_s| Status::Bad).parse(i)
43}
44fn status_preauth(i: &[u8]) -> IResult<&[u8], Status> {
45    map(tag_no_case("PREAUTH"), |_s| Status::PreAuth).parse(i)
46}
47fn status_bye(i: &[u8]) -> IResult<&[u8], Status> {
48    map(tag_no_case("BYE"), |_s| Status::Bye).parse(i)
49}
50
51#[derive(Debug, Eq, PartialEq)]
52pub enum Status {
53    Ok,
54    No,
55    Bad,
56    PreAuth,
57    Bye,
58}
59
60impl Status {
61    fn parse(i: &[u8]) -> IResult<&[u8], Self> {
62        alt((status_ok, status_no, status_bad, status_preauth, status_bye)).parse(i)
63    }
64}
65
66pub(crate) fn mailbox(i: &[u8]) -> IResult<&[u8], Cow<'_, str>> {
67    map(astring_utf8, |s| {
68        if s.eq_ignore_ascii_case("INBOX") {
69            Cow::Borrowed("INBOX")
70        } else {
71            s
72        }
73    })
74    .parse(i)
75}
76
77fn flag_extension(i: &[u8]) -> IResult<&[u8], &str> {
78    map_res(
79        recognize(pair(tag("\\"), take_while(is_atom_char))),
80        from_utf8,
81    )
82    .parse(i)
83}
84
85pub(crate) fn flag(i: &[u8]) -> IResult<&[u8], &str> {
86    // Correct code is
87    //   alt((flag_extension, atom))(i)
88    //
89    // Unfortunately, some unknown providers send the following response:
90    // * FLAGS (OIB-Seen-[Gmail]/All)
91    //
92    // As a workaround, ']' (resp-specials) is allowed here.
93    alt((
94        flag_extension,
95        map_res(take_while1(is_astring_char), from_utf8),
96    ))
97    .parse(i)
98}
99
100fn flag_list(i: &[u8]) -> IResult<&[u8], Vec<Cow<'_, str>>> {
101    // Correct code is
102    //   parenthesized_list(flag)(i)
103    //
104    // Unfortunately, Zoho Mail Server (imap.zoho.com) sends the following response:
105    // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)
106    //
107    // As a workaround, "\*" is allowed here.
108    //
109    // Also, surgemail sends an additional space before the closing bracket:
110    // * FLAGS (\Answered \Flagged \Deleted \Draft \Seen $Forwarded )
111    //
112    // As a workaround, optional spaces before the closing bracket are allowed.
113    parenthesized_list(map(flag_perm, Cow::Borrowed)).parse(i)
114}
115
116fn flag_perm(i: &[u8]) -> IResult<&[u8], &str> {
117    alt((map_res(tag("\\*"), from_utf8), flag)).parse(i)
118}
119
120fn resp_text_code_alert(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
121    map(tag_no_case("ALERT"), |_| ResponseCode::Alert).parse(i)
122}
123
124fn resp_text_code_badcharset(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
125    map(
126        preceded(
127            tag_no_case("BADCHARSET"),
128            opt(preceded(
129                tag(" "),
130                parenthesized_nonempty_list(astring_utf8),
131            )),
132        ),
133        ResponseCode::BadCharset,
134    )
135    .parse(i)
136}
137
138fn resp_text_code_capability(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
139    map(capability_data, ResponseCode::Capabilities).parse(i)
140}
141
142fn resp_text_code_parse(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
143    map(tag_no_case("PARSE"), |_| ResponseCode::Parse).parse(i)
144}
145
146fn resp_text_code_permanent_flags(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
147    map(
148        preceded(
149            tag_no_case("PERMANENTFLAGS "),
150            parenthesized_list(map(flag_perm, Cow::Borrowed)),
151        ),
152        ResponseCode::PermanentFlags,
153    )
154    .parse(i)
155}
156
157fn resp_text_code_read_only(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
158    map(tag_no_case("READ-ONLY"), |_| ResponseCode::ReadOnly).parse(i)
159}
160
161fn resp_text_code_read_write(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
162    map(tag_no_case("READ-WRITE"), |_| ResponseCode::ReadWrite).parse(i)
163}
164
165fn resp_text_code_try_create(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
166    map(tag_no_case("TRYCREATE"), |_| ResponseCode::TryCreate).parse(i)
167}
168
169fn resp_text_code_uid_validity(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
170    map(
171        preceded(tag_no_case("UIDVALIDITY "), number),
172        ResponseCode::UidValidity,
173    )
174    .parse(i)
175}
176
177fn resp_text_code_uid_next(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
178    map(
179        preceded(tag_no_case("UIDNEXT "), number),
180        ResponseCode::UidNext,
181    )
182    .parse(i)
183}
184
185fn resp_text_code_unseen(i: &[u8]) -> IResult<&[u8], ResponseCode<'_>> {
186    map(
187        preceded(tag_no_case("UNSEEN "), number),
188        ResponseCode::Unseen,
189    )
190    .parse(i)
191}
192
193#[derive(Debug, Eq, PartialEq)]
194#[non_exhaustive]
195pub enum ResponseCode<'a> {
196    Alert,
197    BadCharset(Option<Vec<Cow<'a, str>>>),
198    Capabilities(Vec<Capability<'a>>),
199    HighestModSeq(u64), // RFC 4551, section 3.1.1
200    Parse,
201    PermanentFlags(Vec<Cow<'a, str>>),
202    ReadOnly,
203    ReadWrite,
204    TryCreate,
205    UidNext(u32),
206    UidValidity(u32),
207    Unseen(u32),
208    AppendUid(u32, Vec<UidSetMember>),
209    CopyUid(u32, Vec<UidSetMember>, Vec<UidSetMember>),
210    UidNotSticky,
211    MetadataLongEntries(u64), // RFC 5464, section 4.2.1
212    MetadataMaxSize(u64),     // RFC 5464, section 4.3
213    MetadataTooMany,          // RFC 5464, section 4.3
214    MetadataNoPrivate,        // RFC 5464, section 4.3
215}
216
217impl<'a> ResponseCode<'a> {
218    fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
219        // Per the spec, the closing tag should be "] ".
220        // See `Outcome::parse` for more on why this is done differently.
221        delimited(
222            tag("["),
223            alt((
224                resp_text_code_alert,
225                resp_text_code_badcharset,
226                resp_text_code_capability,
227                resp_text_code_parse,
228                resp_text_code_permanent_flags,
229                resp_text_code_uid_validity,
230                resp_text_code_uid_next,
231                resp_text_code_unseen,
232                resp_text_code_read_only,
233                resp_text_code_read_write,
234                resp_text_code_try_create,
235                rfc4551::resp_text_code_highest_mod_seq,
236                rfc4315::resp_text_code_append_uid,
237                rfc4315::resp_text_code_copy_uid,
238                rfc4315::resp_text_code_uid_not_sticky,
239                rfc5464::resp_text_code_metadata_long_entries,
240                rfc5464::resp_text_code_metadata_max_size,
241                rfc5464::resp_text_code_metadata_too_many,
242                rfc5464::resp_text_code_metadata_no_private,
243            )),
244            tag("]"),
245        )
246        .parse(i)
247    }
248
249    pub fn into_owned(self) -> ResponseCode<'static> {
250        match self {
251            ResponseCode::Alert => ResponseCode::Alert,
252            ResponseCode::BadCharset(v) => {
253                ResponseCode::BadCharset(v.map(|vs| vs.into_iter().map(to_owned_cow).collect()))
254            }
255            ResponseCode::Capabilities(v) => {
256                ResponseCode::Capabilities(v.into_iter().map(Capability::into_owned).collect())
257            }
258            ResponseCode::HighestModSeq(v) => ResponseCode::HighestModSeq(v),
259            ResponseCode::Parse => ResponseCode::Parse,
260            ResponseCode::PermanentFlags(v) => {
261                ResponseCode::PermanentFlags(v.into_iter().map(to_owned_cow).collect())
262            }
263            ResponseCode::ReadOnly => ResponseCode::ReadOnly,
264            ResponseCode::ReadWrite => ResponseCode::ReadWrite,
265            ResponseCode::TryCreate => ResponseCode::TryCreate,
266            ResponseCode::UidNext(v) => ResponseCode::UidNext(v),
267            ResponseCode::UidValidity(v) => ResponseCode::UidValidity(v),
268            ResponseCode::Unseen(v) => ResponseCode::Unseen(v),
269            ResponseCode::AppendUid(a, b) => ResponseCode::AppendUid(a, b),
270            ResponseCode::CopyUid(a, b, c) => ResponseCode::CopyUid(a, b, c),
271            ResponseCode::UidNotSticky => ResponseCode::UidNotSticky,
272            ResponseCode::MetadataLongEntries(v) => ResponseCode::MetadataLongEntries(v),
273            ResponseCode::MetadataMaxSize(v) => ResponseCode::MetadataMaxSize(v),
274            ResponseCode::MetadataTooMany => ResponseCode::MetadataTooMany,
275            ResponseCode::MetadataNoPrivate => ResponseCode::MetadataNoPrivate,
276        }
277    }
278}
279
280#[derive(Debug, Eq, PartialEq, Hash)]
281pub enum Capability<'a> {
282    Imap4rev1,
283    Auth(Cow<'a, str>),
284    Atom(Cow<'a, str>),
285}
286
287impl<'a> Capability<'a> {
288    fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
289        alt((
290            map(tag_no_case("IMAP4rev1"), |_| Capability::Imap4rev1),
291            map(
292                map(preceded(tag_no_case("AUTH="), atom), Cow::Borrowed),
293                Capability::Auth,
294            ),
295            map(map(atom, Cow::Borrowed), Capability::Atom),
296        ))
297        .parse(i)
298    }
299
300    pub fn into_owned(self) -> Capability<'static> {
301        match self {
302            Capability::Imap4rev1 => Capability::Imap4rev1,
303            Capability::Auth(v) => Capability::Auth(to_owned_cow(v)),
304            Capability::Atom(v) => Capability::Atom(to_owned_cow(v)),
305        }
306    }
307}
308
309fn ensure_capabilities_contains_imap4rev(
310    capabilities: Vec<Capability<'_>>,
311) -> Result<Vec<Capability<'_>>, ()> {
312    if capabilities.contains(&Capability::Imap4rev1) {
313        Ok(capabilities)
314    } else {
315        Err(())
316    }
317}
318
319fn capability_data(i: &[u8]) -> IResult<&[u8], Vec<Capability<'_>>> {
320    map_res(
321        preceded(
322            tag_no_case("CAPABILITY"),
323            many0(preceded(char(' '), Capability::parse)),
324        ),
325        ensure_capabilities_contains_imap4rev,
326    )
327    .parse(i)
328}
329
330fn mailbox_data_search(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
331    map(
332        // Technically, trailing whitespace is not allowed here, but multiple
333        // email servers in the wild seem to have it anyway (see #34, #108).
334        terminated(
335            preceded(tag_no_case("SEARCH"), many0(preceded(tag(" "), number))),
336            opt(tag(" ")),
337        ),
338        MailboxDatum::Search,
339    )
340    .parse(i)
341}
342
343fn mailbox_data_flags(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
344    map(
345        preceded(tag_no_case("FLAGS "), flag_list),
346        MailboxDatum::Flags,
347    )
348    .parse(i)
349}
350
351fn mailbox_data_exists(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
352    map(
353        terminated(number, tag_no_case(" EXISTS")),
354        MailboxDatum::Exists,
355    )
356    .parse(i)
357}
358
359/// The name attributes are returned as part of a LIST response described in
360/// [RFC 3501 section 7.2.2](https://tools.ietf.org/html/rfc3501#section-7.2.2).
361///
362/// This enumeration additional includes values from the extension Special-Use
363/// Mailboxes [RFC 6154 section 2](https://tools.ietf.org/html/rfc6154#section-2).
364#[derive(Debug, Eq, PartialEq, Clone)]
365#[non_exhaustive]
366pub enum NameAttribute<'a> {
367    /// From [RFC 3501 section 7.2.2](https://tools.ietf.org/html/rfc3501#section-7.2.2):
368    ///
369    /// > It is not possible for any child levels of hierarchy to exist
370    /// > under this name; no child levels exist now and none can be
371    /// > created in the future.
372    NoInferiors,
373    /// From [RFC 3501 section 7.2.2](https://tools.ietf.org/html/rfc3501#section-7.2.2):
374    ///
375    /// > It is not possible to use this name as a selectable mailbox.
376    NoSelect,
377    /// From [RFC 3501 section 7.2.2](https://tools.ietf.org/html/rfc3501#section-7.2.2):
378    ///
379    /// > The mailbox has been marked "interesting" by the server; the
380    /// > mailbox probably contains messages that have been added since
381    /// > the last time the mailbox was selected.
382    Marked,
383    /// From [RFC 3501 section 7.2.2](https://tools.ietf.org/html/rfc3501#section-7.2.2):
384    ///
385    /// > The mailbox does not contain any additional messages since the
386    /// > last time the mailbox was selected.
387    Unmarked,
388    /// From [RFC 6154 section 2](https://tools.ietf.org/html/rfc6154#section-2):
389    ///
390    /// > This mailbox presents all messages in the user's message store.
391    /// > Implementations MAY omit some messages, such as, perhaps, those
392    /// > in \Trash and \Junk.  When this special use is supported, it is
393    /// > almost certain to represent a virtual mailbox.
394    All,
395    /// From [RFC 6154 section 2](https://tools.ietf.org/html/rfc6154#section-2):
396    ///
397    /// > This mailbox is used to archive messages.  The meaning of an
398    /// > "archival" mailbox is server-dependent; typically, it will be
399    /// > used to get messages out of the inbox, or otherwise keep them
400    /// > out of the user's way, while still making them accessible.
401    Archive,
402    /// From [RFC 6154 section 2](https://tools.ietf.org/html/rfc6154#section-2):
403    ///
404    /// > This mailbox is used to hold draft messages -- typically,
405    /// > messages that are being composed but have not yet been sent.  In
406    /// > some server implementations, this might be a virtual mailbox,
407    /// > containing messages from other mailboxes that are marked with
408    /// > the "\Draft" message flag.  Alternatively, this might just be
409    /// > advice that a client put drafts here.
410    Drafts,
411    /// From [RFC 6154 section 2](https://tools.ietf.org/html/rfc6154#section-2):
412    ///
413    /// > This mailbox presents all messages marked in some way as
414    /// > "important".  When this special use is supported, it is likely
415    /// > to represent a virtual mailbox collecting messages (from other
416    /// > mailboxes) that are marked with the "\Flagged" message flag.
417    Flagged,
418    /// From [RFC 6154 section 2](https://tools.ietf.org/html/rfc6154#section-2):
419    ///
420    /// > This mailbox is where messages deemed to be junk mail are held.
421    /// > Some server implementations might put messages here
422    /// > automatically.  Alternatively, this might just be advice to a
423    /// > client-side spam filter.
424    Junk,
425    /// From [RFC 6154 section 2](https://tools.ietf.org/html/rfc6154#section-2):
426    ///
427    /// > This mailbox is used to hold copies of messages that have been
428    /// > sent.  Some server implementations might put messages here
429    /// > automatically.  Alternatively, this might just be advice that a
430    /// > client save sent messages here.
431    Sent,
432    /// From [RFC 6154 section 2](https://tools.ietf.org/html/rfc6154#section-2)
433    ///
434    /// > This mailbox is used to hold messages that have been deleted or
435    /// > marked for deletion.  In some server implementations, this might
436    /// > be a virtual mailbox, containing messages from other mailboxes
437    /// > that are marked with the "\Deleted" message flag.
438    /// > Alternatively, this might just be advice that a client that
439    /// > chooses not to use the IMAP "\Deleted" model should use this as
440    /// > its trash location.  In server implementations that strictly
441    /// > expect the IMAP "\Deleted" model, this special use is likely not
442    /// > to be supported.
443    Trash,
444    /// A name attribute not defined in [RFC 3501 section 7.2.2](https://tools.ietf.org/html/rfc3501#section-7.2.2)
445    /// or any supported extension.
446    Extension(Cow<'a, str>),
447}
448
449impl<'a> NameAttribute<'a> {
450    fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
451        alt((
452            // RFC 3501
453            value(NameAttribute::NoInferiors, tag_no_case("\\Noinferiors")),
454            value(NameAttribute::NoSelect, tag_no_case("\\Noselect")),
455            value(NameAttribute::Marked, tag_no_case("\\Marked")),
456            value(NameAttribute::Unmarked, tag_no_case("\\Unmarked")),
457            // RFC 6154
458            value(NameAttribute::All, tag_no_case("\\All")),
459            value(NameAttribute::Archive, tag_no_case("\\Archive")),
460            value(NameAttribute::Drafts, tag_no_case("\\Drafts")),
461            value(NameAttribute::Flagged, tag_no_case("\\Flagged")),
462            value(NameAttribute::Junk, tag_no_case("\\Junk")),
463            value(NameAttribute::Sent, tag_no_case("\\Sent")),
464            value(NameAttribute::Trash, tag_no_case("\\Trash")),
465            // Extensions not supported by this crate
466            map(
467                map_res(
468                    recognize(pair(tag("\\"), take_while(is_atom_char))),
469                    from_utf8,
470                ),
471                |s| NameAttribute::Extension(Cow::Borrowed(s)),
472            ),
473        ))
474        .parse(i)
475    }
476
477    pub fn into_owned(self) -> NameAttribute<'static> {
478        match self {
479            // RFC 3501
480            NameAttribute::NoInferiors => NameAttribute::NoInferiors,
481            NameAttribute::NoSelect => NameAttribute::NoSelect,
482            NameAttribute::Marked => NameAttribute::Marked,
483            NameAttribute::Unmarked => NameAttribute::Unmarked,
484            // RFC 6154
485            NameAttribute::All => NameAttribute::All,
486            NameAttribute::Archive => NameAttribute::Archive,
487            NameAttribute::Drafts => NameAttribute::Drafts,
488            NameAttribute::Flagged => NameAttribute::Flagged,
489            NameAttribute::Junk => NameAttribute::Junk,
490            NameAttribute::Sent => NameAttribute::Sent,
491            NameAttribute::Trash => NameAttribute::Trash,
492            // Extensions not supported by this crate
493            NameAttribute::Extension(s) => NameAttribute::Extension(to_owned_cow(s)),
494        }
495    }
496}
497
498#[derive(Clone, Debug, Eq, PartialEq)]
499pub struct MailboxListData<'a> {
500    pub name_attributes: Vec<NameAttribute<'a>>,
501    pub delimiter: Option<Cow<'a, str>>,
502    pub name: Cow<'a, str>,
503}
504
505impl<'a> MailboxListData<'a> {
506    fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
507        map(
508            (
509                parenthesized_list(NameAttribute::parse),
510                tag(" "),
511                alt((map(quoted_utf8, Some), map(nil, |_| None))),
512                tag(" "),
513                mailbox,
514            ),
515            |(name_attributes, _, delimiter, _, name)| MailboxListData {
516                name_attributes,
517                delimiter,
518                name,
519            },
520        )
521        .parse(i)
522    }
523
524    pub fn into_owned(self) -> MailboxListData<'static> {
525        MailboxListData {
526            name_attributes: self
527                .name_attributes
528                .into_iter()
529                .map(|named_attribute| named_attribute.into_owned())
530                .collect(),
531            delimiter: self.delimiter.map(to_owned_cow),
532            name: to_owned_cow(self.name),
533        }
534    }
535}
536
537fn mailbox_data_list(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
538    map(
539        preceded(tag_no_case("LIST "), MailboxListData::parse),
540        MailboxDatum::List,
541    )
542    .parse(i)
543}
544
545fn mailbox_data_lsub(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
546    map(
547        preceded(tag_no_case("LSUB "), MailboxListData::parse),
548        MailboxDatum::List,
549    )
550    .parse(i)
551}
552
553#[derive(Debug, Eq, PartialEq, Clone)]
554#[non_exhaustive]
555pub enum StatusAttribute {
556    HighestModSeq(u64), // RFC 4551
557    Messages(u32),
558    Recent(u32),
559    UidNext(u32),
560    UidValidity(u32),
561    Unseen(u32),
562}
563
564impl StatusAttribute {
565    // Unlike `status_att` in the RFC syntax, this includes the value,
566    // so that it can return a valid enum object instead of just a key.
567    fn parse(i: &[u8]) -> IResult<&[u8], Self> {
568        alt((
569            rfc4551::status_att_val_highest_mod_seq,
570            map(
571                preceded(tag_no_case("MESSAGES "), number),
572                StatusAttribute::Messages,
573            ),
574            map(
575                preceded(tag_no_case("RECENT "), number),
576                StatusAttribute::Recent,
577            ),
578            map(
579                preceded(tag_no_case("UIDNEXT "), number),
580                StatusAttribute::UidNext,
581            ),
582            map(
583                preceded(tag_no_case("UIDVALIDITY "), number),
584                StatusAttribute::UidValidity,
585            ),
586            map(
587                preceded(tag_no_case("UNSEEN "), number),
588                StatusAttribute::Unseen,
589            ),
590        ))
591        .parse(i)
592    }
593}
594
595fn status_att_list(i: &[u8]) -> IResult<&[u8], Vec<StatusAttribute>> {
596    // RFC 3501 specifies that the list is non-empty in the formal grammar
597    //   status-att-list =  status-att SP number *(SP status-att SP number)
598    // but mail.163.com sends an empty list in STATUS response anyway.
599    parenthesized_list(StatusAttribute::parse).parse(i)
600}
601
602fn mailbox_data_status(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
603    map(
604        (tag_no_case("STATUS "), mailbox, tag(" "), status_att_list),
605        |(_, mailbox, _, status)| MailboxDatum::Status { mailbox, status },
606    )
607    .parse(i)
608}
609
610fn mailbox_data_recent(i: &[u8]) -> IResult<&[u8], MailboxDatum<'_>> {
611    map(
612        terminated(number, tag_no_case(" RECENT")),
613        MailboxDatum::Recent,
614    )
615    .parse(i)
616}
617
618#[derive(Debug, Eq, PartialEq, Clone)]
619#[non_exhaustive]
620pub enum MailboxDatum<'a> {
621    Exists(u32),
622    Flags(Vec<Cow<'a, str>>),
623    List(MailboxListData<'a>),
624    Search(Vec<u32>),
625    Sort(Vec<u32>),
626    Status {
627        mailbox: Cow<'a, str>,
628        status: Vec<StatusAttribute>,
629    },
630    Recent(u32),
631    MetadataSolicited {
632        mailbox: Cow<'a, str>,
633        values: Vec<Metadata>,
634    },
635    MetadataUnsolicited {
636        mailbox: Cow<'a, str>,
637        values: Vec<Cow<'a, str>>,
638    },
639    GmailLabels(Vec<Cow<'a, str>>),
640    GmailMsgId(u64),
641    GmailThrId(u64),
642}
643
644impl<'a> MailboxDatum<'a> {
645    fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
646        alt((
647            mailbox_data_flags,
648            mailbox_data_exists,
649            mailbox_data_list,
650            mailbox_data_lsub,
651            mailbox_data_status,
652            mailbox_data_recent,
653            mailbox_data_search,
654            gmail::mailbox_data_gmail_labels,
655            gmail::mailbox_data_gmail_msgid,
656            gmail::mailbox_data_gmail_thrid,
657            rfc5256::mailbox_data_sort,
658        ))
659        .parse(i)
660    }
661
662    pub fn into_owned(self) -> MailboxDatum<'static> {
663        match self {
664            MailboxDatum::Exists(seq) => MailboxDatum::Exists(seq),
665            MailboxDatum::Flags(flags) => {
666                MailboxDatum::Flags(flags.into_iter().map(to_owned_cow).collect())
667            }
668            MailboxDatum::List(data) => MailboxDatum::List(data.into_owned()),
669            MailboxDatum::Search(seqs) => MailboxDatum::Search(seqs),
670            MailboxDatum::Sort(seqs) => MailboxDatum::Sort(seqs),
671            MailboxDatum::Status { mailbox, status } => MailboxDatum::Status {
672                mailbox: to_owned_cow(mailbox),
673                status,
674            },
675            MailboxDatum::Recent(seq) => MailboxDatum::Recent(seq),
676            MailboxDatum::MetadataSolicited { mailbox, values } => {
677                MailboxDatum::MetadataSolicited {
678                    mailbox: to_owned_cow(mailbox),
679                    values,
680                }
681            }
682            MailboxDatum::MetadataUnsolicited { mailbox, values } => {
683                MailboxDatum::MetadataUnsolicited {
684                    mailbox: to_owned_cow(mailbox),
685                    values: values.into_iter().map(to_owned_cow).collect(),
686                }
687            }
688            MailboxDatum::GmailLabels(labels) => {
689                MailboxDatum::GmailLabels(labels.into_iter().map(to_owned_cow).collect())
690            }
691            MailboxDatum::GmailMsgId(msgid) => MailboxDatum::GmailMsgId(msgid),
692            MailboxDatum::GmailThrId(thrid) => MailboxDatum::GmailThrId(thrid),
693        }
694    }
695}
696
697#[derive(Debug, Eq, PartialEq)]
698pub struct Address<'a> {
699    pub name: Option<Cow<'a, [u8]>>,
700    pub adl: Option<Cow<'a, [u8]>>,
701    pub mailbox: Option<Cow<'a, [u8]>>,
702    pub host: Option<Cow<'a, [u8]>>,
703}
704
705impl<'a> Address<'a> {
706    // An address structure is a parenthesized list that describes an
707    // electronic mail address.
708    fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
709        paren_delimited(map(
710            (
711                nstring,
712                tag(" "),
713                nstring,
714                tag(" "),
715                nstring,
716                tag(" "),
717                nstring,
718            ),
719            |(name, _, adl, _, mailbox, _, host)| Address {
720                name: name.map(Cow::Borrowed),
721                adl: adl.map(Cow::Borrowed),
722                mailbox: mailbox.map(Cow::Borrowed),
723                host: host.map(Cow::Borrowed),
724            },
725        ))
726        .parse(i)
727    }
728
729    pub fn into_owned(self) -> Address<'static> {
730        Address {
731            name: self.name.map(to_owned_cow),
732            adl: self.adl.map(to_owned_cow),
733            mailbox: self.mailbox.map(to_owned_cow),
734            host: self.host.map(to_owned_cow),
735        }
736    }
737}
738
739fn opt_addresses(i: &[u8]) -> IResult<&[u8], Option<Vec<Address<'_>>>> {
740    alt((
741        map(nil, |_s| None),
742        map(
743            paren_delimited(many1(terminated(Address::parse, opt(char(' '))))),
744            Some,
745        ),
746    ))
747    .parse(i)
748}
749
750/// An RFC 2822 envelope
751///
752/// See <https://datatracker.ietf.org/doc/html/rfc2822#section-3.6> for more details.
753#[derive(Debug, Eq, PartialEq)]
754pub struct Envelope<'a> {
755    pub date: Option<Cow<'a, [u8]>>,
756    pub subject: Option<Cow<'a, [u8]>>,
757    /// Author of the message; mailbox responsible for writing the message
758    pub from: Option<Vec<Address<'a>>>,
759    /// Mailbox of the agent responsible for the message's transmission
760    pub sender: Option<Vec<Address<'a>>>,
761    /// Mailbox that the author of the message suggests replies be sent to
762    pub reply_to: Option<Vec<Address<'a>>>,
763    pub to: Option<Vec<Address<'a>>>,
764    pub cc: Option<Vec<Address<'a>>>,
765    pub bcc: Option<Vec<Address<'a>>>,
766    pub in_reply_to: Option<Cow<'a, [u8]>>,
767    pub message_id: Option<Cow<'a, [u8]>>,
768}
769
770impl<'a> Envelope<'a> {
771    // envelope        = "(" env-date SP env-subject SP env-from SP
772    //                   env-sender SP env-reply-to SP env-to SP env-cc SP
773    //                   env-bcc SP env-in-reply-to SP env-message-id ")"
774    //
775    // env-bcc         = "(" 1*address ")" / nil
776    //
777    // env-cc          = "(" 1*address ")" / nil
778    //
779    // env-date        = nstring
780    //
781    // env-from        = "(" 1*address ")" / nil
782    //
783    // env-in-reply-to = nstring
784    //
785    // env-message-id  = nstring
786    //
787    // env-reply-to    = "(" 1*address ")" / nil
788    //
789    // env-sender      = "(" 1*address ")" / nil
790    //
791    // env-subject     = nstring
792    //
793    // env-to          = "(" 1*address ")" / nil
794    pub(crate) fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
795        paren_delimited(map(
796            (
797                nstring,
798                tag(" "),
799                nstring,
800                tag(" "),
801                opt_addresses,
802                tag(" "),
803                opt_addresses,
804                tag(" "),
805                opt_addresses,
806                tag(" "),
807                opt_addresses,
808                tag(" "),
809                opt_addresses,
810                tag(" "),
811                opt_addresses,
812                tag(" "),
813                nstring,
814                tag(" "),
815                nstring,
816            ),
817            |(
818                date,
819                _,
820                subject,
821                _,
822                from,
823                _,
824                sender,
825                _,
826                reply_to,
827                _,
828                to,
829                _,
830                cc,
831                _,
832                bcc,
833                _,
834                in_reply_to,
835                _,
836                message_id,
837            )| Envelope {
838                date: date.map(Cow::Borrowed),
839                subject: subject.map(Cow::Borrowed),
840                from,
841                sender,
842                reply_to,
843                to,
844                cc,
845                bcc,
846                in_reply_to: in_reply_to.map(Cow::Borrowed),
847                message_id: message_id.map(Cow::Borrowed),
848            },
849        ))
850        .parse(i)
851    }
852
853    pub fn into_owned(self) -> Envelope<'static> {
854        Envelope {
855            date: self.date.map(to_owned_cow),
856            subject: self.subject.map(to_owned_cow),
857            from: self
858                .from
859                .map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
860            sender: self
861                .sender
862                .map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
863            reply_to: self
864                .reply_to
865                .map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
866            to: self
867                .to
868                .map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
869            cc: self
870                .cc
871                .map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
872            bcc: self
873                .bcc
874                .map(|v| v.into_iter().map(|v| v.into_owned()).collect()),
875            in_reply_to: self.in_reply_to.map(to_owned_cow),
876            message_id: self.message_id.map(to_owned_cow),
877        }
878    }
879}
880
881fn msg_att_envelope(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
882    map(
883        preceded(tag_no_case("ENVELOPE "), Envelope::parse),
884        |envelope| AttributeValue::Envelope(Box::new(envelope)),
885    )
886    .parse(i)
887}
888
889fn msg_att_internal_date(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
890    map(
891        preceded(tag_no_case("INTERNALDATE "), nstring_utf8),
892        |date| AttributeValue::InternalDate(date.unwrap()),
893    )
894    .parse(i)
895}
896
897fn msg_att_flags(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
898    map(
899        preceded(tag_no_case("FLAGS "), flag_list),
900        AttributeValue::Flags,
901    )
902    .parse(i)
903}
904
905fn msg_att_rfc822(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
906    map(preceded(tag_no_case("RFC822 "), nstring), |v| {
907        AttributeValue::Rfc822(v.map(Cow::Borrowed))
908    })
909    .parse(i)
910}
911
912fn msg_att_rfc822_header(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
913    // extra space workaround for DavMail
914    map(
915        (tag_no_case("RFC822.HEADER "), opt(tag(" ")), nstring),
916        |(_, _, raw)| AttributeValue::Rfc822Header(raw.map(Cow::Borrowed)),
917    )
918    .parse(i)
919}
920
921fn msg_att_rfc822_size(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
922    map(
923        preceded(tag_no_case("RFC822.SIZE "), number),
924        AttributeValue::Rfc822Size,
925    )
926    .parse(i)
927}
928
929fn msg_att_rfc822_text(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
930    map(preceded(tag_no_case("RFC822.TEXT "), nstring), |v| {
931        AttributeValue::Rfc822Text(v.map(Cow::Borrowed))
932    })
933    .parse(i)
934}
935
936fn msg_att_uid(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
937    map(preceded(tag_no_case("UID "), number), AttributeValue::Uid).parse(i)
938}
939
940// msg-att         = "(" (msg-att-dynamic / msg-att-static)
941//                    *(SP (msg-att-dynamic / msg-att-static)) ")"
942//
943// msg-att-dynamic = "FLAGS" SP "(" [flag-fetch *(SP flag-fetch)] ")"
944//                     ; MAY change for a message
945//
946// msg-att-static  = "ENVELOPE" SP envelope / "INTERNALDATE" SP date-time /
947//                   "RFC822" [".HEADER" / ".TEXT"] SP nstring /
948//                   "RFC822.SIZE" SP number /
949//                   "BODY" ["STRUCTURE"] SP body /
950//                   "BODY" section ["<" number ">"] SP nstring /
951//                   "UID" SP uniqueid
952//                     ; MUST NOT change for a message
953
954// RFC 8474 §5.1 — EMAILID
955//   "EMAILID" SP "(" objectid ")"
956// objectid = 1*ASTRING-CHAR (RFC 8474 §3).  EMAILID is non-optional: a
957// compliant server MUST be able to provide one for any stored message,
958// so the wire form `EMAILID NIL` is not allowed by the RFC.  If a
959// non-compliant server emits it anyway, this parser fails and the
960// catch-all `msg_att_unknown` below absorbs the attribute.
961fn msg_att_emailid(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
962    map(
963        preceded(
964            tag_no_case("EMAILID "),
965            paren_delimited(map_res(take_while1(is_astring_char), from_utf8)),
966        ),
967        |id| AttributeValue::EmailId(Cow::Borrowed(id)),
968    )
969    .parse(i)
970}
971
972// RFC 8474 §5.2 — THREADID
973//   "THREADID" SP ( "(" objectid ")" / nil )
974// THREADID may be NIL, which the RFC mandates for messages that do not
975// currently have a thread association.  We map NIL → `None`.
976fn msg_att_threadid(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
977    map(
978        preceded(
979            tag_no_case("THREADID "),
980            alt((
981                map(nil, |_| None),
982                map(
983                    paren_delimited(map_res(take_while1(is_astring_char), from_utf8)),
984                    |id| Some(Cow::Borrowed(id)),
985                ),
986            )),
987        ),
988        AttributeValue::ThreadId,
989    )
990    .parse(i)
991}
992
993// Catch-all for RFC extension attributes not explicitly handled above.
994//
995// RFC-compliant servers (Apache James, Stalwart, Dovecot) may include
996// attributes from extensions that post-date this crate, for example:
997//   - RFC 8514 §2 SAVEDATE
998//   - any future extension this crate has not yet typed
999//
1000// When all known parsers fail, this function consumes "name SP value" where
1001// the value is one of:
1002//   - a single-level parenthesised group `(...)`
1003//   - an nstring (NIL / quoted-string / literal) — covers SAVEDATE and NIL forms
1004//   - a bare atom or number — fallback for any remaining scalar value
1005//
1006// The name and value are discarded and `AttributeValue::Unknown` is returned so
1007// that the rest of the `FETCH` attribute list can be parsed without error.
1008// Note: deeply nested parenthesised values (beyond one level) are not handled
1009// by the bare-paren arm; add a dedicated parser if a specific extension needs them.
1010fn msg_att_unknown(i: &[u8]) -> IResult<&[u8], AttributeValue<'_>> {
1011    map(
1012        pair(
1013            map_res(take_while1(is_atom_char), from_utf8),
1014            preceded(
1015                tag(" "),
1016                alt((
1017                    value((), paren_delimited(take_while(|c: u8| c != b')'))),
1018                    value((), nstring),
1019                    value((), take_while1(|c: u8| c != b' ' && c != b')')),
1020                )),
1021            ),
1022        ),
1023        |_| AttributeValue::Unknown,
1024    )
1025    .parse(i)
1026}
1027
1028#[allow(clippy::large_enum_variant)]
1029#[derive(Debug, Eq, PartialEq)]
1030#[non_exhaustive]
1031pub enum AttributeValue<'a> {
1032    BodySection {
1033        section: Option<SectionPath>,
1034        index: Option<u32>,
1035        data: Option<Cow<'a, [u8]>>,
1036    },
1037    BodyStructure(BodyStructure<'a>),
1038    Envelope(Box<Envelope<'a>>),
1039    Flags(Vec<Cow<'a, str>>),
1040    InternalDate(Cow<'a, str>),
1041    ModSeq(u64), // RFC 4551, section 3.3.2
1042    Rfc822(Option<Cow<'a, [u8]>>),
1043    Rfc822Header(Option<Cow<'a, [u8]>>),
1044    Rfc822Size(u32),
1045    Rfc822Text(Option<Cow<'a, [u8]>>),
1046    Uid(u32),
1047    /// <https://developers.google.com/gmail/imap/imap-extensions#access_to_gmail_labels_x-gm-labels>
1048    GmailLabels(Vec<Cow<'a, str>>),
1049    GmailMsgId(u64),
1050    GmailThrId(u64),
1051    /// RFC 8474 §5.1 — `EMAILID`: a server-assigned unique identifier for
1052    /// a message. RFC 8474 mandates that the server can always provide an
1053    /// EMAILID for any stored message, so this variant is non-optional.
1054    EmailId(Cow<'a, str>),
1055    /// RFC 8474 §5.2 — `THREADID`: a server-assigned identifier for the
1056    /// thread a message belongs to.  `None` corresponds to the wire-form
1057    /// `THREADID NIL`, which RFC 8474 §5.2 mandates for messages that do
1058    /// not currently have a thread association.
1059    ThreadId(Option<Cow<'a, str>>),
1060    /// An unknown or not-yet-supported FETCH attribute.
1061    ///
1062    /// Returned for any `msg-att` token that the parser does not explicitly
1063    /// recognise (e.g. `SAVEDATE` from RFC 8514, or any future extension).
1064    /// The name and raw value are consumed and discarded so that the rest
1065    /// of the `FETCH` attribute list can be parsed without error.  Callers
1066    /// that need the raw value should match on the specific RFC extension
1067    /// and open a tracking issue or PR.
1068    Unknown,
1069}
1070
1071impl<'a> AttributeValue<'a> {
1072    fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
1073        alt((
1074            msg_att_body_section,
1075            msg_att_body_structure,
1076            msg_att_envelope,
1077            msg_att_internal_date,
1078            msg_att_flags,
1079            rfc4551::msg_att_mod_seq,
1080            msg_att_rfc822,
1081            msg_att_rfc822_header,
1082            msg_att_rfc822_size,
1083            msg_att_rfc822_text,
1084            msg_att_uid,
1085            gmail::msg_att_gmail_labels,
1086            gmail::msg_att_gmail_msgid,
1087            gmail::msg_att_gmail_thrid,
1088            msg_att_emailid,
1089            msg_att_threadid,
1090            msg_att_unknown,
1091        ))
1092        .parse(i)
1093    }
1094
1095    pub fn into_owned(self) -> AttributeValue<'static> {
1096        match self {
1097            AttributeValue::BodySection {
1098                section,
1099                index,
1100                data,
1101            } => AttributeValue::BodySection {
1102                section,
1103                index,
1104                data: data.map(to_owned_cow),
1105            },
1106            AttributeValue::BodyStructure(body) => AttributeValue::BodyStructure(body.into_owned()),
1107            AttributeValue::Envelope(e) => AttributeValue::Envelope(Box::new(e.into_owned())),
1108            AttributeValue::Flags(v) => {
1109                AttributeValue::Flags(v.into_iter().map(to_owned_cow).collect())
1110            }
1111            AttributeValue::InternalDate(v) => AttributeValue::InternalDate(to_owned_cow(v)),
1112            AttributeValue::ModSeq(v) => AttributeValue::ModSeq(v),
1113            AttributeValue::Rfc822(v) => AttributeValue::Rfc822(v.map(to_owned_cow)),
1114            AttributeValue::Rfc822Header(v) => AttributeValue::Rfc822Header(v.map(to_owned_cow)),
1115            AttributeValue::Rfc822Size(v) => AttributeValue::Rfc822Size(v),
1116            AttributeValue::Rfc822Text(v) => AttributeValue::Rfc822Text(v.map(to_owned_cow)),
1117            AttributeValue::Uid(v) => AttributeValue::Uid(v),
1118            AttributeValue::GmailLabels(v) => {
1119                AttributeValue::GmailLabels(v.into_iter().map(to_owned_cow).collect())
1120            }
1121            AttributeValue::GmailMsgId(v) => AttributeValue::GmailMsgId(v),
1122            AttributeValue::GmailThrId(v) => AttributeValue::GmailThrId(v),
1123            AttributeValue::EmailId(v) => AttributeValue::EmailId(to_owned_cow(v)),
1124            AttributeValue::ThreadId(v) => AttributeValue::ThreadId(v.map(to_owned_cow)),
1125            AttributeValue::Unknown => AttributeValue::Unknown,
1126        }
1127    }
1128}
1129
1130fn msg_att_list(i: &[u8]) -> IResult<&[u8], Vec<AttributeValue<'_>>> {
1131    parenthesized_nonempty_list(AttributeValue::parse).parse(i)
1132}
1133
1134// message-data    = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
1135fn message_data_fetch(i: &[u8]) -> IResult<&[u8], Response<'_>> {
1136    map(
1137        (number, tag_no_case(" FETCH "), msg_att_list),
1138        |(num, _, attrs)| Response::Fetch(num, attrs),
1139    )
1140    .parse(i)
1141}
1142
1143// message-data    = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
1144fn message_data_expunge(i: &[u8]) -> IResult<&[u8], u32> {
1145    terminated(number, tag_no_case(" EXPUNGE")).parse(i)
1146}
1147
1148#[derive(Clone, Debug, Eq, PartialEq)]
1149pub struct RequestId(pub String);
1150
1151impl RequestId {
1152    // tag             = 1*<any ASTRING-CHAR except "+">
1153    fn parse(i: &[u8]) -> IResult<&[u8], Self> {
1154        map(map_res(take_while1(is_tag_char), from_utf8), |s| {
1155            RequestId(s.to_string())
1156        })
1157        .parse(i)
1158    }
1159
1160    pub fn as_bytes(&self) -> &[u8] {
1161        self.0.as_bytes()
1162    }
1163}
1164
1165#[derive(Debug, Default, Eq, PartialEq)]
1166pub struct Outcome<'a> {
1167    pub code: Option<ResponseCode<'a>>,
1168    pub information: Option<Cow<'a, str>>,
1169}
1170
1171impl<'a> Outcome<'a> {
1172    // This is not quite according to spec, which mandates the following:
1173    //     ["[" resp-text-code "]" SP] text
1174    // However, examples in RFC 4551 (Conditional STORE) counteract this by giving
1175    // examples of `resp-text` that do not include the trailing space and text.
1176    fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> {
1177        map((opt(ResponseCode::parse), text), |(code, text)| {
1178            let information = if text.is_empty() {
1179                None
1180            } else if code.is_some() {
1181                Some(match text {
1182                    Cow::Borrowed(s) => Cow::Borrowed(&s[1..]),
1183                    Cow::Owned(s) => Cow::Owned(s[1..].to_string()),
1184                })
1185            } else {
1186                Some(text)
1187            };
1188
1189            Outcome { code, information }
1190        })
1191        .parse(i)
1192    }
1193
1194    pub fn into_owned(self) -> Outcome<'static> {
1195        Outcome {
1196            code: self.code.map(ResponseCode::into_owned),
1197            information: self.information.map(to_owned_cow),
1198        }
1199    }
1200}
1201
1202// an response-text if it is at the end of a response. Empty text is then allowed without the normally needed trailing space.
1203fn trailing_resp_text(i: &[u8]) -> IResult<&[u8], Outcome<'_>> {
1204    map(opt((tag(" "), Outcome::parse)), |resptext| {
1205        resptext.map(|(_, tuple)| tuple).unwrap_or_default()
1206    })
1207    .parse(i)
1208}
1209
1210// continue-req    = "+" SP (resp-text / base64) CRLF
1211pub(crate) fn continue_req(i: &[u8]) -> IResult<&[u8], Response<'_>> {
1212    // Some servers do not send the space :/
1213    // TODO: base64
1214    map(
1215        (tag("+"), opt(tag(" ")), Outcome::parse, tag("\r\n")),
1216        |(_, _, outcome, _)| Response::Continue(outcome),
1217    )
1218    .parse(i)
1219}
1220
1221// response-tagged = tag SP resp-cond-state CRLF
1222//
1223// resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
1224//                     ; Status condition
1225pub(crate) fn response_tagged(i: &[u8]) -> IResult<&[u8], Response<'_>> {
1226    map(
1227        (
1228            RequestId::parse,
1229            tag(" "),
1230            Status::parse,
1231            trailing_resp_text,
1232            tag("\r\n"),
1233        ),
1234        |(tag, _, status, outcome, _)| Response::Done {
1235            tag,
1236            status,
1237            outcome,
1238        },
1239    )
1240    .parse(i)
1241}
1242
1243// resp-cond-auth  = ("OK" / "PREAUTH") SP resp-text
1244//                     ; Authentication condition
1245//
1246// resp-cond-bye   = "BYE" SP resp-text
1247//
1248// resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text
1249//                     ; Status condition
1250fn resp_cond(i: &[u8]) -> IResult<&[u8], Response<'_>> {
1251    map((Status::parse, trailing_resp_text), |(status, outcome)| {
1252        Response::Data { status, outcome }
1253    })
1254    .parse(i)
1255}
1256
1257// response-data   = "*" SP (resp-cond-state / resp-cond-bye /
1258//                   mailbox-data / message-data / capability-data / quota) CRLF
1259pub(crate) fn response_data(i: &[u8]) -> IResult<&[u8], Response<'_>> {
1260    delimited(
1261        tag("* "),
1262        alt((
1263            resp_cond,
1264            map(MailboxDatum::parse, Response::MailboxData),
1265            map(message_data_expunge, Response::Expunge),
1266            message_data_fetch,
1267            map(capability_data, Response::Capabilities),
1268            rfc5161::resp_enabled,
1269            rfc5464::metadata_solicited,
1270            rfc5464::metadata_unsolicited,
1271            rfc7162::resp_vanished,
1272            rfc2087::quota,
1273            rfc2087::quota_root,
1274            rfc2971::resp_id,
1275            rfc4314::acl,
1276            rfc4314::list_rights,
1277            rfc4314::my_rights,
1278        )),
1279        preceded(
1280            many0(tag(" ")), // Outlook server sometimes sends whitespace at the end of STATUS response.
1281            tag("\r\n"),
1282        ),
1283    )
1284    .parse(i)
1285}
1286
1287#[cfg(test)]
1288mod tests {
1289    use super::{AttributeValue, Capability, NameAttribute};
1290    use assert_matches::assert_matches;
1291    use std::borrow::Cow;
1292
1293    #[test]
1294    fn test_list() {
1295        match super::mailbox(b"iNboX ") {
1296            Ok((_, mb)) => {
1297                assert_eq!(mb, "INBOX");
1298            }
1299            rsp => panic!("unexpected response {rsp:?}"),
1300        }
1301    }
1302
1303    #[test]
1304    fn test_envelope() {
1305        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>") "#;
1306        match super::msg_att_envelope(env) {
1307            Ok((_, AttributeValue::Envelope(_))) => {}
1308            rsp => panic!("unexpected response {rsp:?}"),
1309        }
1310    }
1311
1312    #[test]
1313    fn test_opt_addresses() {
1314        let addr = b"((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") (\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) ";
1315        match super::opt_addresses(addr) {
1316            Ok((_, _addresses)) => {}
1317            rsp => panic!("unexpected response {rsp:?}"),
1318        }
1319    }
1320
1321    #[test]
1322    fn test_opt_addresses_no_space() {
1323        let addr =
1324            br#"((NIL NIL "test" "example@example.com")(NIL NIL "test" "example@example.com"))"#;
1325        match super::opt_addresses(addr) {
1326            Ok((_, _addresses)) => {}
1327            rsp => panic!("unexpected response {rsp:?}"),
1328        }
1329    }
1330
1331    #[test]
1332    fn test_addresses() {
1333        match super::Address::parse(b"(\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\") ") {
1334            Ok((_, _address)) => {}
1335            rsp => panic!("unexpected response {rsp:?}"),
1336        }
1337
1338        // Literal non-UTF8 address
1339        match super::Address::parse(b"({12}\r\nJoh\xff Klensin NIL \"KLENSIN\" \"MIT.EDU\") ") {
1340            Ok((_, _address)) => {}
1341            rsp => panic!("unexpected response {rsp:?}"),
1342        }
1343    }
1344
1345    #[test]
1346    fn test_capability_data() {
1347        // Minimal capabilities
1348        assert_matches!(
1349            super::capability_data(b"CAPABILITY IMAP4rev1\r\n"),
1350            Ok((_, capabilities)) => {
1351                assert_eq!(capabilities, vec![Capability::Imap4rev1])
1352            }
1353        );
1354
1355        assert_matches!(
1356            super::capability_data(b"CAPABILITY XPIG-LATIN IMAP4rev1 STARTTLS AUTH=GSSAPI\r\n"),
1357            Ok((_, capabilities)) => {
1358                assert_eq!(capabilities, vec![
1359                    Capability::Atom(Cow::Borrowed("XPIG-LATIN")),
1360                    Capability::Imap4rev1,
1361                    Capability::Atom(Cow::Borrowed("STARTTLS")),
1362                    Capability::Auth(Cow::Borrowed("GSSAPI")),
1363                ])
1364            }
1365        );
1366
1367        assert_matches!(
1368            super::capability_data(b"CAPABILITY IMAP4rev1 AUTH=GSSAPI AUTH=PLAIN\r\n"),
1369            Ok((_, capabilities)) => {
1370                assert_eq!(capabilities, vec![
1371                    Capability::Imap4rev1,
1372                    Capability::Auth(Cow::Borrowed("GSSAPI")),
1373                    Capability::Auth(Cow::Borrowed("PLAIN")),
1374                ])
1375            }
1376        );
1377
1378        // Capability command must contain IMAP4rev1
1379        assert_matches!(
1380            super::capability_data(b"CAPABILITY AUTH=GSSAPI AUTH=PLAIN\r\n"),
1381            Err(_)
1382        );
1383    }
1384
1385    #[test]
1386    fn test_surgemail_select_flags() {
1387        // Tests workaround for surgemail with space before closing bracket
1388        assert_matches!(
1389            super::flag_list(b"(\\Answered \\Flagged \\Deleted \\Draft \\Seen $Forwarded )"),
1390            Ok(([], flags)) => {
1391                assert_eq!(flags, vec![
1392                        "\\Answered",
1393                        "\\Flagged",
1394                        "\\Deleted",
1395                        "\\Draft",
1396                        "\\Seen",
1397                        "$Forwarded"
1398                    ])
1399            }
1400        );
1401    }
1402
1403    /// Tests that the [`NameAttribute::into_owned`] method returns the
1404    /// same value (the ownership should only change).
1405    #[test]
1406    fn test_name_attribute_into_owned() {
1407        let name_attributes = [
1408            // RFC 3501
1409            NameAttribute::NoInferiors,
1410            NameAttribute::NoSelect,
1411            NameAttribute::Marked,
1412            NameAttribute::Unmarked,
1413            // RFC 6154
1414            NameAttribute::All,
1415            NameAttribute::Archive,
1416            NameAttribute::Drafts,
1417            NameAttribute::Flagged,
1418            NameAttribute::Junk,
1419            NameAttribute::Sent,
1420            NameAttribute::Trash,
1421            // Extensions not supported by this crate
1422            NameAttribute::Extension(Cow::Borrowed("Foobar")),
1423        ];
1424
1425        for name_attribute in name_attributes {
1426            let owned_name_attribute = name_attribute.clone().into_owned();
1427            assert_eq!(name_attribute, owned_name_attribute);
1428        }
1429    }
1430
1431    #[test]
1432    fn test_attribute_value_unknown_into_owned() {
1433        assert_eq!(
1434            AttributeValue::Unknown.into_owned(),
1435            AttributeValue::Unknown
1436        );
1437    }
1438}