Skip to main content

daaki_imap/types/
response.rs

1//! IMAP server response types (RFC 3501 Section 7 / RFC 9051 Section 7).
2//!
3//! Models the full grammar of IMAP responses: greeting, tagged, untagged, and continuation.
4//! Extended response codes per RFC 5530.
5
6use super::validated::MailboxName;
7use super::{FetchResponse, Flag, MailboxInfo, StatusItem};
8
9/// A complete response line from the IMAP server
10/// (RFC 3501 Section 2.2.2 / RFC 9051 Section 2.2.2).
11#[non_exhaustive]
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub enum Response {
15    /// Initial server greeting on connection (RFC 3501 Section 7.1).
16    Greeting(GreetingResponse),
17    /// Tagged response to a client command (RFC 3501 Section 2.2.2).
18    Tagged(TaggedResponse),
19    /// Untagged (unsolicited or data) response (RFC 3501 Section 2.2.2).
20    Untagged(Box<UntaggedResponse>),
21    /// Continuation request (`+ ...`) (RFC 3501 Section 7.5).
22    Continuation(ContinuationRequest),
23}
24
25/// Initial greeting sent by the server upon connection
26/// (RFC 3501 Section 7.1 / RFC 9051 Section 7.1).
27#[non_exhaustive]
28#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct GreetingResponse {
31    /// Greeting status (`OK`, `PREAUTH`, or `BYE`) (RFC 3501 Section 7.1 / RFC 9051 Section 7.1).
32    pub status: GreetingStatus,
33    /// Optional response code in square brackets (RFC 3501 Section 7.1 / RFC 9051 Section 7.1).
34    pub code: Option<ResponseCode>,
35    /// Human-readable text following the status (RFC 3501 Section 7.1 / RFC 9051 Section 7.1).
36    pub text: String,
37}
38
39/// Status of the server greeting (RFC 3501 Section 7.1 / RFC 9051 Section 7.1).
40#[non_exhaustive]
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub enum GreetingStatus {
44    /// `* OK` — server ready, client should authenticate.
45    #[default]
46    Ok,
47    /// `* PREAUTH` — already authenticated (e.g. via TLS client cert).
48    PreAuth,
49    /// `* BYE` — server refusing connections.
50    Bye,
51}
52
53/// Tagged response (response to a specific client command)
54/// (RFC 3501 Section 7.1 / RFC 9051 Section 7.1).
55#[non_exhaustive]
56#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct TaggedResponse {
59    /// Command tag that this response corresponds to (RFC 3501 Section 2.2.1 / RFC 9051 Section 2.2.1).
60    pub tag: String,
61    /// Completion status (`OK`, `NO`, or `BAD`) (RFC 3501 Section 7.1 / RFC 9051 Section 7.1).
62    pub status: StatusKind,
63    /// Optional response code in square brackets (RFC 3501 Section 7.1 / RFC 9051 Section 7.1).
64    pub code: Option<ResponseCode>,
65    /// Human-readable text following the status (RFC 3501 Section 7.1 / RFC 9051 Section 7.1).
66    pub text: String,
67}
68
69impl TaggedResponse {
70    /// Check that the response indicates `OK` status. On success returns the
71    /// response itself so callers can still access fields like `code`. On
72    /// failure returns an appropriate [`Error`] for `NO` / `BAD`.
73    ///
74    /// RFC 3501 Section 7.1 / RFC 9051 Section 7.1: `OK` indicates success,
75    /// `NO` an operational error, `BAD` a protocol-level error.
76    pub(crate) fn require_ok(self) -> Result<Self, crate::error::Error> {
77        match self.status {
78            StatusKind::Ok => Ok(self),
79            StatusKind::No => Err(crate::error::Error::no_with_code(self.text, self.code)),
80            StatusKind::Bad => Err(crate::error::Error::bad_with_code(self.text, self.code)),
81        }
82    }
83}
84
85/// Status of a tagged response (RFC 3501 Section 7.1 / RFC 9051 Section 7.1).
86#[non_exhaustive]
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub enum StatusKind {
90    #[default]
91    Ok,
92    No,
93    Bad,
94}
95
96/// Status of an untagged status response (RFC 3501 Section 7.1 / RFC 9051 Section 7.1).
97#[non_exhaustive]
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100pub enum UntaggedStatus {
101    #[default]
102    Ok,
103    No,
104    Bad,
105    Bye,
106}
107
108/// Untagged server response (RFC 3501 Section 7 / RFC 9051 Section 7).
109#[non_exhaustive]
110#[derive(Debug, Clone, PartialEq, Eq, Hash)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub enum UntaggedResponse {
113    /// `* OK/NO/BAD/BYE [code] text` (RFC 3501 Section 7.1).
114    Status {
115        status: UntaggedStatus,
116        code: Option<ResponseCode>,
117        text: String,
118    },
119    /// `* <n> EXISTS` (RFC 3501 Section 7.3.1).
120    Exists(u32),
121    /// `* <n> RECENT` (RFC 3501 Section 7.3.2).
122    Recent(u32),
123    /// `* <n> EXPUNGE` (RFC 3501 Section 7.4.1).
124    Expunge(u32),
125    /// `* <n> FETCH (...)` (RFC 3501 Section 7.4.2).
126    Fetch(Box<FetchResponse>),
127    /// `* LIST (\attrs) "/" "name"` (RFC 3501 Section 7.2.2).
128    List(MailboxInfo),
129    /// `* LSUB (\attrs) "/" "name"` (RFC 3501 Section 7.2.3).
130    Lsub(MailboxInfo),
131    /// `* FLAGS (...)` (RFC 3501 Section 7.2.6).
132    Flags(Vec<Flag>),
133    /// `* SEARCH 1 2 3 ... [(MODSEQ n)]` (RFC 3501 Section 7.2.5, RFC 7162 Section 3.1.5).
134    ///
135    /// The optional `mod_seq` is present when the client searched with a MODSEQ
136    /// criterion and the result is non-empty (RFC 7162 Section 3.1.5).
137    Search {
138        /// Matching message sequence numbers or UIDs.
139        uids: Vec<u32>,
140        /// Highest mod-sequence of matching messages (RFC 7162 Section 3.1.5).
141        mod_seq: Option<u64>,
142    },
143    /// `* ESEARCH (TAG "tag") [UID] result-data` (RFC 4731 Section 3.1).
144    ///
145    /// RFC 4731 Section 3.1 ABNF:
146    /// `search-return-data = "MIN" SP nz-number / "MAX" SP nz-number /
147    ///                        "ALL" SP sequence-set / "COUNT" SP number`
148    Esearch(EsearchResponse),
149    /// `* STATUS "mailbox" (...)` (RFC 3501 Section 7.2.4).
150    MailboxStatus {
151        mailbox: MailboxName,
152        items: Vec<StatusItem>,
153    },
154    /// `* CAPABILITY ...` (RFC 3501 Section 7.2.1).
155    Capability(Vec<Capability>),
156    /// `* ENABLED ...` (RFC 5161 Section 3.2).
157    Enabled(Vec<String>),
158    /// `* VANISHED (EARLIER) 1:5` (RFC 7162 QRESYNC).
159    Vanished { earlier: bool, uids: Vec<UidRange> },
160    /// `* ID (...)` (RFC 2971 Section 3.2).
161    Id(Vec<(String, Option<String>)>),
162    /// `* NAMESPACE personal other shared` (RFC 2342).
163    Namespace {
164        personal: Vec<NamespaceDescriptor>,
165        other: Vec<NamespaceDescriptor>,
166        shared: Vec<NamespaceDescriptor>,
167    },
168
169    // --- QUOTA (RFC 2087) ---
170    /// `* QUOTA <root> (STORAGE <usage> <limit>)` (RFC 2087 Section 5.1).
171    Quota {
172        root: String,
173        resources: Vec<QuotaResource>,
174    },
175    /// `* QUOTAROOT <mailbox> <root1> <root2> ...` (RFC 2087 Section 5.2).
176    QuotaRoot {
177        mailbox: MailboxName,
178        roots: Vec<String>,
179    },
180
181    // --- ACL (RFC 4314) ---
182    /// `* ACL <mailbox> <id1> <rights1> ...` (RFC 4314 Section 3.6).
183    Acl {
184        mailbox: MailboxName,
185        entries: Vec<AclEntry>,
186    },
187    /// `* MYRIGHTS <mailbox> <rights>` (RFC 4314 Section 3.8).
188    MyRights {
189        mailbox: MailboxName,
190        rights: String,
191    },
192    /// `* LISTRIGHTS <mailbox> <id> <required> <optional1> ...` (RFC 4314 Section 3.7).
193    ListRights {
194        mailbox: MailboxName,
195        identifier: String,
196        required: String,
197        optional: Vec<String>,
198    },
199    /// `* METADATA "mailbox" (entry1 value1 ...)` (RFC 5464 Section 4.4).
200    Metadata {
201        mailbox: MailboxName,
202        entries: Vec<MetadataEntry>,
203    },
204    /// `* THREAD (...)` (RFC 5256 Section 4).
205    Thread(Vec<ThreadNode>),
206    /// SORT response — sorted message numbers and optional MODSEQ
207    /// (RFC 5256 Section 4, RFC 7162 Section 3.1.6).
208    ///
209    /// RFC 7162 Section 3.1.6: when a MODSEQ search criterion is used and the
210    /// SORT result is non-empty, the server appends `(MODSEQ <n>)`.
211    Sort {
212        /// Sorted message numbers or UIDs (RFC 5256 Section 4).
213        nums: Vec<u32>,
214        /// Highest mod-sequence value of matching messages (RFC 7162 Section 3.1.6).
215        mod_seq: Option<u64>,
216    },
217    /// Unknown or unrecognized untagged response (RFC 9051 Section 2.2.2).
218    ///
219    /// Servers may send extension responses that this client does not yet
220    /// implement. Per RFC 9051, clients MUST tolerate such responses.
221    Unknown(String),
222}
223
224/// Continuation request from the server (RFC 3501 Section 7.5 / RFC 9051 Section 7.5).
225///
226/// RFC 3501 Section 7.5: `continue-req = "+" SP (resp-text / base64) CRLF`
227/// where `resp-text = ["[" resp-text-code "]" SP] text`.
228#[non_exhaustive]
229#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
230#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
231pub struct ContinuationRequest {
232    /// Optional response code in square brackets (RFC 3501 Section 7.5 / RFC 9051 Section 7.5).
233    ///
234    /// Present when the server sends a continuation like `+ [ALERT] text\r\n`.
235    /// Base64 SASL challenges never start with `[`, so this is `None` for those.
236    pub code: Option<ResponseCode>,
237    /// Text or base64 challenge from the server
238    /// (RFC 3501 Section 7.5 / RFC 9051 Section 7.5.1).
239    pub data: String,
240}
241
242/// Response code in square brackets (e.g. `[UIDVALIDITY 12345]`)
243/// (RFC 3501 Section 7.1 / RFC 9051 Section 7.1).
244#[non_exhaustive]
245#[derive(Debug, Clone, PartialEq, Eq, Hash)]
246#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
247pub enum ResponseCode {
248    /// `[ALERT]` — must be presented to the user (RFC 3501 Section 7.1).
249    Alert,
250    /// `[BADCHARSET (charsets)]` — search charset not supported (RFC 3501 Section 7.1).
251    BadCharset(Vec<String>),
252    /// `[CAPABILITY ...]` — capability list (RFC 3501 Section 7.1).
253    Capability(Vec<Capability>),
254    /// `[PARSE]` — message headers could not be parsed (RFC 3501 Section 7.1).
255    Parse,
256    /// `[PERMANENTFLAGS (flags)]` — flags the client can change permanently (RFC 3501 Section 7.1).
257    PermanentFlags(Vec<Flag>),
258    /// `[READ-ONLY]` — mailbox is read-only (RFC 3501 Section 7.1).
259    ReadOnly,
260    /// `[READ-WRITE]` — mailbox is read-write (RFC 3501 Section 7.1).
261    ReadWrite,
262    /// `[TRYCREATE]` — attempt to CREATE the target mailbox (RFC 3501 Section 7.1).
263    TryCreate,
264    /// `[UIDNEXT n]` — predicted next UID (RFC 3501 Section 7.1).
265    UidNext(u32),
266    /// `[UIDVALIDITY n]` — UID validity value (RFC 3501 Section 7.1).
267    UidValidity(u32),
268    /// `[UNSEEN n]` — first unseen message sequence number (RFC 3501 Section 7.1).
269    Unseen(u32),
270    /// `[APPENDUID uidvalidity uid-set]` (RFC 4315 UIDPLUS Section 3).
271    ///
272    /// For a single APPEND, `uids` contains one range. For MULTIAPPEND
273    /// (RFC 3502), `uids` may contain multiple ranges.
274    AppendUid {
275        uid_validity: u32,
276        uids: Vec<UidRange>,
277    },
278    /// `[COPYUID uidvalidity source-uids dest-uids]` (RFC 4315 UIDPLUS).
279    CopyUid {
280        uid_validity: u32,
281        source_uids: Vec<UidRange>,
282        dest_uids: Vec<UidRange>,
283    },
284    /// `[HIGHESTMODSEQ n]` (RFC 7162 CONDSTORE).
285    HighestModSeq(u64),
286    /// `[MODIFIED sequence-set]` (RFC 7162 Section 3.1.3 / Section 7).
287    ///
288    /// For `STORE`, the set contains message sequence numbers. For `UID STORE`,
289    /// it contains UIDs.
290    Modified(Vec<UidRange>),
291    /// `[NOMODSEQ]` — mailbox does not support mod-sequences (RFC 7162 Section 3.1.2).
292    NoModSeq,
293    /// `[CLOSED]` — previously selected mailbox is now closed (RFC 7162 QRESYNC).
294    Closed,
295    /// `[MAILBOXID (objectid)]` — unique mailbox identifier (RFC 8474 Section 5.1).
296    MailboxId(String),
297
298    // --- RFC 5530 extended response codes ---
299    /// `[UNAVAILABLE]` — server temporarily unavailable (RFC 5530 Section 3).
300    Unavailable,
301    /// `[AUTHENTICATIONFAILED]` — authentication credentials invalid (RFC 5530 Section 3).
302    AuthenticationFailed,
303    /// `[AUTHORIZATIONFAILED]` — authorization identity not permitted (RFC 5530 Section 3).
304    AuthorizationFailed,
305    /// `[EXPIRED]` — credentials have expired (RFC 5530 Section 3).
306    Expired,
307    /// `[PRIVACYREQUIRED]` — operation requires encryption (RFC 5530 Section 3).
308    PrivacyRequired,
309    /// `[CONTACTADMIN]` — contact server administrator (RFC 5530 Section 3).
310    ContactAdmin,
311    /// `[NOPERM]` — no permission to perform the operation (RFC 5530 Section 3).
312    NoPerm,
313    /// `[INUSE]` — resource is in use by another session (RFC 5530 Section 3).
314    InUse,
315    /// `[EXPUNGEISSUED]` — expunge occurred during operation (RFC 5530 Section 3).
316    ExpungeIssued,
317    /// `[CORRUPTION]` — server detected data corruption (RFC 5530 Section 3).
318    Corruption,
319    /// `[SERVERBUG]` — server encountered an internal bug (RFC 5530 Section 3).
320    ServerBug,
321    /// `[CLIENTBUG]` — client sent malformed or nonsensical data (RFC 5530 Section 3).
322    ClientBug,
323    /// `[CANNOT]` — operation is not supported on this mailbox/server (RFC 5530 Section 3).
324    Cannot,
325    /// `[LIMIT]` — operation exceeds a server-imposed limit (RFC 5530 Section 3).
326    Limit,
327    /// `[OVERQUOTA]` — user has exceeded their storage quota (RFC 5530 Section 3).
328    OverQuota,
329    /// `[ALREADYEXISTS]` — mailbox already exists (e.g. on CREATE) (RFC 5530 Section 3).
330    AlreadyExists,
331    /// `[NONEXISTENT]` — mailbox does not exist (e.g. on SELECT/DELETE) (RFC 5530 Section 3).
332    NonExistent,
333    /// `[NEWNAME ...]` — registered response code, obsolete but still standardized;
334    /// trailing data is preserved verbatim (RFC 5530 Section 6).
335    NewName(Option<String>),
336    /// `[REFERRAL ...]` — registered response code; trailing data is preserved
337    /// verbatim for consumers (RFC 5530 Section 6).
338    Referral(Option<String>),
339    /// `[URLMECH ...]` — registered response code; trailing data is preserved
340    /// verbatim for consumers (RFC 5530 Section 6).
341    UrlMech(Option<String>),
342    /// `[BADURL ...]` — registered response code; trailing data is preserved
343    /// verbatim for consumers (RFC 5530 Section 6).
344    BadUrl(Option<String>),
345    /// `[BADCOMPARATOR ...]` — registered response code; trailing data is
346    /// preserved verbatim for consumers (RFC 5530 Section 6).
347    BadComparator(Option<String>),
348    /// `[ANNOTATE ...]` — registered response code; trailing data is preserved
349    /// verbatim for consumers (RFC 5530 Section 6).
350    Annotate(Option<String>),
351    /// `[ANNOTATIONS ...]` — registered response code; trailing data is preserved
352    /// verbatim for consumers (RFC 5530 Section 6).
353    Annotations(Option<String>),
354    /// `[TEMPFAIL ...]` — registered response code; trailing data is preserved
355    /// verbatim for consumers (RFC 5530 Section 6).
356    TempFail(Option<String>),
357    /// `[MAXCONVERTMESSAGES ...]` — registered response code; trailing data is
358    /// preserved verbatim for consumers (RFC 5530 Section 6).
359    MaxConvertMessages(Option<String>),
360    /// `[MAXCONVERTPARTS ...]` — registered response code; trailing data is
361    /// preserved verbatim for consumers (RFC 5530 Section 6).
362    MaxConvertParts(Option<String>),
363    /// `[NOUPDATE ...]` — registered response code; trailing data is preserved
364    /// verbatim for consumers (RFC 5530 Section 6).
365    NoUpdate(Option<String>),
366    /// `[NOTIFICATIONOVERFLOW ...]` — registered response code; trailing data
367    /// is preserved verbatim for consumers (RFC 5465 Section 5.8 / RFC 5530 Section 6).
368    NotificationOverflow(Option<String>),
369    /// `[BADEVENT ...]` — registered response code; trailing data is preserved
370    /// verbatim for consumers (RFC 5465 Section 5 / RFC 5530 Section 6).
371    BadEvent(Option<String>),
372    /// `[UNDEFINED-FILTER ...]` — registered response code; trailing data is
373    /// preserved verbatim for consumers (RFC 5465 Section 8 / RFC 5530 Section 6).
374    UndefinedFilter(Option<String>),
375
376    /// `[UIDNOTSTICKY]` — assigned UIDs are not persistent (RFC 4315 Section 2 / RFC 9051 Section 7.1).
377    UidNotSticky,
378    /// `[NOTSAVED]` — search result variable `$` is empty (RFC 5182 Section 2.1).
379    NotSaved,
380    /// `[HASCHILDREN]` — mailbox has child mailboxes (RFC 9051 Section 7.1).
381    HasChildren,
382    /// `[UNKNOWN-CTE]` — BINARY fetch failed due to unknown CTE (RFC 3516 Section 4.3).
383    UnknownCte,
384    /// `[TOOBIG]` — message too large for APPEND (RFC 7889 Section 4).
385    TooBig,
386    /// `[COMPRESSIONACTIVE]` — compression layer already active (RFC 4978 Section 3).
387    CompressionActive,
388    /// `[USEATTR]` — special-use attribute not supported (RFC 6154 Section 6).
389    UseAttr,
390
391    // --- METADATA (RFC 5464) ---
392    /// `[METADATA LONGENTRIES n]` — entry values were truncated at `n` bytes
393    /// (RFC 5464 Section 4.2.1).
394    MetadataLongEntries(u64),
395    /// `[METADATA MAXSIZE n]` — server's maximum annotation size
396    /// (RFC 5464 Section 4.3).
397    MetadataMaxSize(u64),
398    /// `[METADATA TOOMANY]` — too many annotations on this mailbox
399    /// (RFC 5464 Section 4.3).
400    MetadataTooMany,
401    /// `[METADATA NOPRIVATE]` — server does not support private annotations
402    /// (RFC 5464 Section 4.3).
403    MetadataNoPrivate,
404
405    /// Unrecognized response code — preserved for forward compatibility.
406    Other { name: String, value: Option<String> },
407}
408
409/// Server capability (RFC 3501 Section 7.2.1 / RFC 9051 Section 7.2.1).
410///
411/// Comparison and hashing are case-insensitive per RFC 3501 Section 7.2.1.
412#[non_exhaustive]
413#[derive(Debug, Clone)]
414#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
415pub enum Capability {
416    /// `IMAP4rev1` (RFC 3501).
417    Imap4Rev1,
418    /// `IMAP4rev2` (RFC 9051).
419    Imap4Rev2,
420    // --- Extensions (alphabetical) ---
421    /// `ACL` (RFC 4314).
422    Acl,
423    /// `APPENDLIMIT` (RFC 7889 Section 5).
424    AppendLimit(Option<u64>),
425    /// `BINARY` (RFC 3516).
426    Binary,
427    /// `CHILDREN` (RFC 3348).
428    Children,
429    /// `COMPRESS=DEFLATE` (RFC 4978).
430    CompressDeflate,
431    /// `CONDSTORE` (RFC 7162).
432    Condstore,
433    /// `CREATE-SPECIAL-USE` (RFC 6154).
434    CreateSpecialUse,
435    /// `ENABLE` (RFC 5161).
436    Enable,
437    /// `ESEARCH` (RFC 4731).
438    Esearch,
439    /// `ID` (RFC 2971).
440    Id,
441    /// `IDLE` (RFC 2177).
442    Idle,
443    /// `LIST-EXTENDED` (RFC 5258).
444    ListExtended,
445    /// `LIST-STATUS` (RFC 5819).
446    ListStatus,
447    /// `LITERAL+` (RFC 7888).
448    LiteralPlus,
449    /// `LOGINDISABLED` (RFC 3501 Section 6.2.3 / RFC 9051 Section 6.2.3).
450    LoginDisabled,
451    /// LITERAL- extension — non-synchronizing literals up to 4096 bytes (RFC 7888 Section 5).
452    LiteralMinus,
453    /// `METADATA` (RFC 5464).
454    Metadata,
455    /// `METADATA-SERVER` — server-only metadata annotations (RFC 5464 Section 1).
456    MetadataServer,
457    /// `MOVE` (RFC 6851).
458    Move,
459    /// `MULTIAPPEND` (RFC 3502).
460    MultiAppend,
461    /// `NAMESPACE` (RFC 2342).
462    Namespace,
463    /// `NOTIFY` (RFC 5465).
464    Notify,
465    /// `OBJECTID` (RFC 8474).
466    ObjectId,
467    /// `QRESYNC` (RFC 7162).
468    QResync,
469    /// `QUOTA` (RFC 2087).
470    Quota,
471    /// `QUOTA=RES-<name>` — advertised quota resource type (RFC 9208 Section 3.1.1).
472    QuotaResource(String),
473    /// `QUOTASET` — `SETQUOTA` command support (RFC 9208 Section 4.1.3).
474    QuotaSet,
475    /// `RIGHTS=<chars>` — indicates supported ACL rights (RFC 4314 Section 6).
476    ///
477    /// The `String` holds the new-rights characters (e.g. `"texk"`).
478    Rights(String),
479    /// `PREVIEW` (RFC 8970 Section 4).
480    Preview,
481    /// `SASL-IR` (RFC 4959).
482    SaslIr,
483    /// `SAVEDATE` (RFC 8514).
484    SaveDate,
485    /// `SEARCHRES` (RFC 5182).
486    SearchRes,
487    /// SORT extension (RFC 5256 Section 1).
488    Sort,
489    /// `SORT=DISPLAY` extension (RFC 5957).
490    SortDisplay(String),
491    /// `STARTTLS` (RFC 3501 Section 6.2.1 / RFC 9051 Section 6.2.1).
492    StartTls,
493    /// `SPECIAL-USE` (RFC 6154).
494    SpecialUse,
495    /// `THREAD=<algorithm>` (RFC 5256 Section 1).
496    ///
497    /// The String holds the algorithm name (e.g. `"REFERENCES"`, `"ORDEREDSUBJECT"`).
498    /// Servers may advertise multiple `THREAD=` capabilities, each as a separate entry.
499    Thread(String),
500    /// `STATUS=SIZE` (RFC 8438).
501    StatusSize,
502    /// `UIDPLUS` (RFC 4315).
503    UidPlus,
504    /// `UNAUTHENTICATE` (RFC 8437 Section 2).
505    Unauthenticate,
506    /// `UNSELECT` (RFC 3691).
507    Unselect,
508    /// `UTF8=ACCEPT` (RFC 6855).
509    Utf8Accept,
510    /// `UTF8=ONLY` (RFC 6855 Section 4).
511    Utf8Only,
512    /// `WITHIN` (RFC 5032 Section 3).
513    ///
514    /// Enables OLDER and YOUNGER search keys for time-relative searches.
515    Within,
516    /// `AUTH=<mechanism>` (e.g. `AUTH=PLAIN`, `AUTH=XOAUTH2`) (RFC 3501 Section 7.2.1).
517    Auth(String),
518    /// Unrecognized capability — preserved verbatim.
519    Other(String),
520}
521
522impl Capability {
523    /// Returns the wire representation of this capability
524    /// (e.g. `IDLE`, `AUTH=PLAIN`, `THREAD=REFERENCES`)
525    /// (RFC 3501 Section 7.2.1 / RFC 9051 Section 7.2.1).
526    pub fn as_imap_str(&self) -> String {
527        match self {
528            Self::Imap4Rev1 => "IMAP4rev1".into(),
529            Self::Imap4Rev2 => "IMAP4rev2".into(),
530            Self::Acl => "ACL".into(),
531            Self::AppendLimit(Some(n)) => format!("APPENDLIMIT={n}"),
532            Self::AppendLimit(None) => "APPENDLIMIT".into(),
533            Self::Binary => "BINARY".into(),
534            Self::Children => "CHILDREN".into(),
535            Self::CompressDeflate => "COMPRESS=DEFLATE".into(),
536            Self::Condstore => "CONDSTORE".into(),
537            Self::CreateSpecialUse => "CREATE-SPECIAL-USE".into(),
538            Self::Enable => "ENABLE".into(),
539            Self::Esearch => "ESEARCH".into(),
540            Self::Id => "ID".into(),
541            Self::Idle => "IDLE".into(),
542            Self::ListExtended => "LIST-EXTENDED".into(),
543            Self::ListStatus => "LIST-STATUS".into(),
544            Self::LiteralPlus => "LITERAL+".into(),
545            Self::LoginDisabled => "LOGINDISABLED".into(),
546            Self::LiteralMinus => "LITERAL-".into(),
547            Self::Metadata => "METADATA".into(),
548            Self::MetadataServer => "METADATA-SERVER".into(),
549            Self::Move => "MOVE".into(),
550            Self::MultiAppend => "MULTIAPPEND".into(),
551            Self::Namespace => "NAMESPACE".into(),
552            Self::Notify => "NOTIFY".into(),
553            Self::ObjectId => "OBJECTID".into(),
554            Self::Preview => "PREVIEW".into(),
555            Self::QResync => "QRESYNC".into(),
556            Self::Quota => "QUOTA".into(),
557            Self::QuotaResource(s) => format!("QUOTA=RES-{s}"),
558            Self::QuotaSet => "QUOTASET".into(),
559            Self::Rights(s) => format!("RIGHTS={s}"),
560            Self::SaslIr => "SASL-IR".into(),
561            Self::SaveDate => "SAVEDATE".into(),
562            Self::SearchRes => "SEARCHRES".into(),
563            Self::Sort => "SORT".into(),
564            Self::SortDisplay(s) => format!("SORT={s}"),
565            Self::StartTls => "STARTTLS".into(),
566            Self::SpecialUse => "SPECIAL-USE".into(),
567            Self::Thread(s) => format!("THREAD={s}"),
568            Self::StatusSize => "STATUS=SIZE".into(),
569            Self::Unauthenticate => "UNAUTHENTICATE".into(),
570            Self::UidPlus => "UIDPLUS".into(),
571            Self::Unselect => "UNSELECT".into(),
572            Self::Within => "WITHIN".into(),
573            Self::Utf8Accept => "UTF8=ACCEPT".into(),
574            Self::Utf8Only => "UTF8=ONLY".into(),
575            Self::Auth(s) => format!("AUTH={s}"),
576            Self::Other(s) => s.clone(),
577        }
578    }
579
580    /// Parse a capability token from its IMAP wire representation
581    /// (RFC 3501 Section 7.2.1 / RFC 9051 Section 7.2.2).
582    ///
583    /// Case-insensitive per RFC 3501 Section 7.2.1: "Strstrings in capability
584    /// names are case-insensitive."
585    #[allow(clippy::too_many_lines)]
586    pub fn from_imap_str(s: &str) -> Self {
587        let upper = s.to_ascii_uppercase();
588        match upper.as_str() {
589            "IMAP4REV1" => Self::Imap4Rev1,
590            "IMAP4REV2" => Self::Imap4Rev2,
591            "ACL" => Self::Acl,
592            "BINARY" => Self::Binary,
593            "CHILDREN" => Self::Children,
594            "COMPRESS=DEFLATE" => Self::CompressDeflate,
595            "CONDSTORE" => Self::Condstore,
596            "CREATE-SPECIAL-USE" => Self::CreateSpecialUse,
597            "ENABLE" => Self::Enable,
598            "ESEARCH" => Self::Esearch,
599            "ID" => Self::Id,
600            "IDLE" => Self::Idle,
601            "LIST-EXTENDED" => Self::ListExtended,
602            "LIST-STATUS" => Self::ListStatus,
603            "LITERAL+" => Self::LiteralPlus,
604            "LITERAL-" => Self::LiteralMinus,
605            "LOGINDISABLED" => Self::LoginDisabled,
606            "METADATA" => Self::Metadata,
607            "METADATA-SERVER" => Self::MetadataServer,
608            "MOVE" => Self::Move,
609            "MULTIAPPEND" => Self::MultiAppend,
610            "NAMESPACE" => Self::Namespace,
611            "NOTIFY" => Self::Notify,
612            "OBJECTID" => Self::ObjectId,
613            "PREVIEW" => Self::Preview,
614            "QRESYNC" => Self::QResync,
615            "QUOTA" => Self::Quota,
616            "QUOTASET" => Self::QuotaSet,
617            "SASL-IR" => Self::SaslIr,
618            "SAVEDATE" => Self::SaveDate,
619            "SEARCHRES" => Self::SearchRes,
620            "SORT" => Self::Sort,
621            "SPECIAL-USE" => Self::SpecialUse,
622            "STARTTLS" => Self::StartTls,
623            "STATUS=SIZE" => Self::StatusSize,
624            // RFC 8437 Section 2: UNAUTHENTICATE command support.
625            "UNAUTHENTICATE" => Self::Unauthenticate,
626            "UIDPLUS" => Self::UidPlus,
627            "UNSELECT" => Self::Unselect,
628            // RFC 5032 Section 3: WITHIN enables OLDER/YOUNGER search keys.
629            "WITHIN" => Self::Within,
630            "UTF8=ACCEPT" => Self::Utf8Accept,
631            "UTF8=ONLY" => Self::Utf8Only,
632            _ => {
633                if let Some(mechanism) = upper.strip_prefix("AUTH=") {
634                    // RFC 3501 Section 9 / RFC 9051 Section 9:
635                    // capability = ("AUTH=" auth-type) / atom
636                    // auth-type = atom, so an empty suffix is malformed.
637                    if mechanism.is_empty() {
638                        Self::Other(s.to_owned())
639                    } else {
640                        Self::Auth(mechanism.to_owned())
641                    }
642                } else if upper == "SORT=DISPLAY" {
643                    // RFC 5957 defines the dedicated SORT=DISPLAY capability.
644                    Self::SortDisplay("DISPLAY".to_owned())
645                } else if let Some(resource) = upper.strip_prefix("QUOTA=RES-") {
646                    // RFC 9208 Section 3.1.1: supported quota resources are
647                    // advertised as `QUOTA=RES-<name>`, so the suffix is required.
648                    if resource.is_empty() {
649                        Self::Other(s.to_owned())
650                    } else {
651                        Self::QuotaResource(s["QUOTA=RES-".len()..].to_string())
652                    }
653                } else if let Some(algo) = upper.strip_prefix("THREAD=") {
654                    // THREAD=REFERENCES, THREAD=ORDEREDSUBJECT, etc. (RFC 5256 Section 1)
655                    if algo.is_empty() {
656                        Self::Other(s.to_owned())
657                    } else {
658                        Self::Thread(algo.to_owned())
659                    }
660                } else if let Some(rights) = upper.strip_prefix("RIGHTS=") {
661                    // RIGHTS=<chars> (RFC 4314 Section 6)
662                    // Preserve the original case of the rights characters.
663                    // RFC 4314 Section 7: rights-capa = "RIGHTS=" new-rights,
664                    // and new-rights = 1*LOWER-ALPHA, so an empty suffix is malformed.
665                    if rights.is_empty() {
666                        Self::Other(s.to_owned())
667                    } else {
668                        Self::Rights(s["RIGHTS=".len()..].to_string())
669                    }
670                } else if let Some(rest) = upper.strip_prefix("APPENDLIMIT") {
671                    if rest.is_empty() {
672                        // Bare `APPENDLIMIT` with no `=value` means server-wide limit
673                        // must be checked per-mailbox (RFC 7889 Section 2).
674                        Self::AppendLimit(None)
675                    } else if let Some(val_str) = rest.strip_prefix('=') {
676                        if !val_str.is_empty() && val_str.bytes().all(|b| b.is_ascii_digit()) {
677                            if let Ok(n) = val_str.parse::<u64>() {
678                                Self::AppendLimit(Some(n))
679                            } else {
680                                // Non-numeric — preserve as-is.
681                                Self::Other(s.to_owned())
682                            }
683                        } else {
684                            // Non-numeric — preserve as-is.
685                            Self::Other(s.to_owned())
686                        }
687                    } else {
688                        // RFC 7889 Section 5 only defines bare `APPENDLIMIT`
689                        // and `APPENDLIMIT=<number>`. Any other token starting
690                        // with that prefix is an unknown capability and must be
691                        // preserved verbatim for forward compatibility.
692                        Self::Other(s.to_owned())
693                    }
694                } else {
695                    Self::Other(s.to_owned())
696                }
697            }
698        }
699    }
700}
701
702/// Converts a string to a `Capability` using case-insensitive matching
703/// (RFC 3501 Section 7.2.1).
704impl From<String> for Capability {
705    fn from(s: String) -> Self {
706        Self::from_imap_str(&s)
707    }
708}
709
710/// Converts a string slice to a `Capability` using case-insensitive matching
711/// (RFC 3501 Section 7.2.1).
712impl From<&str> for Capability {
713    fn from(s: &str) -> Self {
714        Self::from_imap_str(s)
715    }
716}
717
718/// RFC 3501 Section 7.2.1: "There is no requirement that capability names be
719/// registered" — capability names are atoms and IMAP atoms are case-insensitive.
720///
721/// Known capability variants with no string payload compare by discriminant.
722/// String-carrying variants (`Auth`, `Thread`, `SortDisplay`, `Rights`, `Other`)
723/// compare using ASCII case-insensitive comparison so that e.g.
724/// `Auth("PLAIN")` and `Auth("plain")` are treated as the same capability.
725///
726/// Cross-representation is also handled: `Other("IDLE")` equals `Idle`,
727/// because they denote the same protocol capability.
728impl PartialEq for Capability {
729    fn eq(&self, other: &Self) -> bool {
730        // RFC 3501 Section 7.2.1: capability comparisons are case-insensitive.
731        match (self, other) {
732            (Self::Imap4Rev1, Self::Imap4Rev1)
733            | (Self::Imap4Rev2, Self::Imap4Rev2)
734            | (Self::Acl, Self::Acl)
735            | (Self::Binary, Self::Binary)
736            | (Self::Children, Self::Children)
737            | (Self::CompressDeflate, Self::CompressDeflate)
738            | (Self::Condstore, Self::Condstore)
739            | (Self::CreateSpecialUse, Self::CreateSpecialUse)
740            | (Self::Enable, Self::Enable)
741            | (Self::Esearch, Self::Esearch)
742            | (Self::Id, Self::Id)
743            | (Self::Idle, Self::Idle)
744            | (Self::ListExtended, Self::ListExtended)
745            | (Self::ListStatus, Self::ListStatus)
746            | (Self::LiteralPlus, Self::LiteralPlus)
747            | (Self::LoginDisabled, Self::LoginDisabled)
748            | (Self::LiteralMinus, Self::LiteralMinus)
749            | (Self::Metadata, Self::Metadata)
750            | (Self::MetadataServer, Self::MetadataServer)
751            | (Self::Move, Self::Move)
752            | (Self::MultiAppend, Self::MultiAppend)
753            | (Self::Namespace, Self::Namespace)
754            | (Self::Notify, Self::Notify)
755            | (Self::ObjectId, Self::ObjectId)
756            | (Self::Preview, Self::Preview)
757            | (Self::QResync, Self::QResync)
758            | (Self::Quota, Self::Quota)
759            | (Self::QuotaSet, Self::QuotaSet)
760            | (Self::SaslIr, Self::SaslIr)
761            | (Self::SaveDate, Self::SaveDate)
762            | (Self::SearchRes, Self::SearchRes)
763            | (Self::Sort, Self::Sort)
764            | (Self::StartTls, Self::StartTls)
765            | (Self::SpecialUse, Self::SpecialUse)
766            | (Self::StatusSize, Self::StatusSize)
767            | (Self::Unauthenticate, Self::Unauthenticate)
768            | (Self::UidPlus, Self::UidPlus)
769            | (Self::Unselect, Self::Unselect)
770            | (Self::Within, Self::Within)
771            | (Self::Utf8Accept, Self::Utf8Accept)
772            | (Self::Utf8Only, Self::Utf8Only) => true,
773            (Self::AppendLimit(a), Self::AppendLimit(b)) => a == b,
774            (Self::Auth(a), Self::Auth(b))
775            | (Self::Thread(a), Self::Thread(b))
776            | (Self::SortDisplay(a), Self::SortDisplay(b))
777            | (Self::QuotaResource(a), Self::QuotaResource(b))
778            | (Self::Rights(a), Self::Rights(b))
779            | (Self::Other(a), Self::Other(b)) => a.eq_ignore_ascii_case(b),
780            // Cross-representation: compare Other's wire form against known variant.
781            (Self::Other(s), known) | (known, Self::Other(s)) => {
782                s.eq_ignore_ascii_case(&known.as_imap_str())
783            }
784            _ => false,
785        }
786    }
787}
788
789/// RFC 3501 Section 7.2.1: capability equality is reflexive, symmetric, transitive.
790impl Eq for Capability {}
791
792/// RFC 3501 Section 7.2.1: capability names are case-insensitive.
793///
794/// The `Hash` implementation must be consistent with `PartialEq`: capabilities that
795/// compare equal must hash to the same value. Because `Other("IDLE")` must
796/// equal `Idle`, we hash the lowercased wire form (`as_imap_str()`) for all
797/// variants, which is identical for cross-representation equivalents.
798impl std::hash::Hash for Capability {
799    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
800        // RFC 3501 Section 7.2.1: case-insensitive hashing via wire form.
801        // Other("IDLE") and Idle both yield "IDLE", so lowercasing
802        // produces the same hash.
803        for byte in self.as_imap_str().as_bytes() {
804            byte.to_ascii_lowercase().hash(state);
805        }
806    }
807}
808
809impl std::fmt::Display for Capability {
810    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
811        std::fmt::Display::fmt(&self.as_imap_str(), f)
812    }
813}
814
815/// A single namespace entry from a NAMESPACE response (RFC 2342).
816///
817/// RFC 2342 Section 6 ABNF:
818/// ```text
819/// Namespace = nil / "(" 1*( "(" string SP  (<"> QUOTED_CHAR <"> / nil)
820///                    *(Namespace_Response_Extension) ")" ) ")"
821/// Namespace_Response_Extension = SP string SP "(" string *(SP string) ")"
822/// ```
823#[non_exhaustive]
824#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
825#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
826pub struct NamespaceDescriptor {
827    /// Namespace prefix (e.g. `""`, `"INBOX."`, `"#shared."`) (RFC 2342 Section 5).
828    pub prefix: String,
829    /// Hierarchy delimiter for this namespace, or `None` if flat (RFC 2342 Section 5).
830    pub delimiter: Option<char>,
831    /// Extension key-value-list pairs (RFC 2342 Section 6).
832    ///
833    /// Each entry is `(key, values)` where key is a string and values is a
834    /// non-empty list of strings, corresponding to one
835    /// `Namespace_Response_Extension = SP string SP "(" string *(SP string) ")"`.
836    pub extensions: Vec<(String, Vec<String>)>,
837}
838
839/// Result of a NAMESPACE command (RFC 2342 Section 5).
840///
841/// Groups the three namespace categories into named fields instead of a
842/// positional tuple, making it safe to extend in the future.
843#[non_exhaustive]
844#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
845#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
846pub struct NamespaceResponse {
847    /// Personal namespaces (RFC 2342 Section 5).
848    pub personal: Vec<NamespaceDescriptor>,
849    /// Other users' namespaces (RFC 2342 Section 5).
850    pub other: Vec<NamespaceDescriptor>,
851    /// Shared namespaces (RFC 2342 Section 5).
852    pub shared: Vec<NamespaceDescriptor>,
853}
854
855/// Result of a GETQUOTAROOT command (RFC 2087 Section 4.3 / RFC 9208 Section 4.1.2).
856///
857/// Contains the quota root names and the quota resources associated with
858/// each root.
859#[non_exhaustive]
860#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
861#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
862pub struct QuotaRootResponse {
863    /// Quota root names returned by the server (RFC 2087 Section 4.3).
864    pub roots: Vec<String>,
865    /// Quota resources keyed by root name (RFC 2087 Section 4.3).
866    ///
867    /// Each entry is `(root_name, resources)` where `resources` is the list
868    /// of resource triplets for that root.
869    pub resources: Vec<(String, Vec<QuotaResource>)>,
870}
871
872/// Result of a LISTRIGHTS command (RFC 4314 Section 3.4).
873///
874/// Contains the rights that are always granted and the groups of optional
875/// rights that can be independently granted.
876#[non_exhaustive]
877#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
878#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
879pub struct ListRightsResponse {
880    /// Rights that are always granted to the identifier (RFC 4314 Section 3.4).
881    pub required: String,
882    /// Groups of optional rights that can be independently granted
883    /// (RFC 4314 Section 3.4).
884    ///
885    /// Each element is a string of right characters that form an indivisible
886    /// group: granting any character in the group grants all of them.
887    pub optional: Vec<String>,
888}
889
890/// ESEARCH response data (RFC 4731 Section 3.1).
891///
892/// RFC 4731 Section 3.1 ABNF:
893/// `search-return-data = "MIN" SP nz-number / "MAX" SP nz-number /
894///                        "ALL" SP sequence-set / "COUNT" SP number`
895///
896/// Normative rules:
897/// - MIN: "Return the lowest message number/UID that satisfies the SEARCH criteria.
898///   If the SEARCH results in no matches, the server MUST NOT include the MIN result
899///   option in the ESEARCH response."
900/// - MAX: "Return the highest message number/UID that satisfies the SEARCH criteria.
901///   If the SEARCH results in no matches, the server MUST NOT include the MAX result
902///   option in the ESEARCH response."
903/// - ALL: Returns matching messages as a sequence-set rather than space-separated.
904///   "If the SEARCH results in no matches, the server MUST NOT include the ALL result
905///   option in the ESEARCH response."
906/// - COUNT: "Return number of the messages that satisfy the SEARCH criteria. This result
907///   option MUST always be included in the ESEARCH response."
908#[non_exhaustive]
909#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
910#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
911pub struct EsearchResponse {
912    /// Correlating tag from `(TAG "tagstring")`, if present
913    /// (RFC 4466 Section 2.6.2 `search-correlator`).
914    pub tag: Option<String>,
915    /// `true` when the response includes the `UID` indicator,
916    /// meaning all returned numbers are UIDs rather than sequence numbers
917    /// (RFC 4731 Section 3.1).
918    pub uid: bool,
919    /// MIN — lowest matching message number/UID (RFC 4731 Section 3.1).
920    pub min: Option<u32>,
921    /// MAX — highest matching message number/UID (RFC 4731 Section 3.1).
922    pub max: Option<u32>,
923    /// COUNT — number of matching messages (RFC 4731 Section 3.1).
924    pub count: Option<u32>,
925    /// ALL — matching message numbers/UIDs as a uid-set (RFC 4731 Section 3.1).
926    pub all: Vec<UidRange>,
927    /// MODSEQ — highest mod-sequence of matching messages (RFC 7162 Section 3.1.10).
928    pub mod_seq: Option<u64>,
929}
930
931/// A UID range (e.g. `1:100`, or a single UID `42`)
932/// (RFC 3501 Section 9 / RFC 4315 Section 2.1).
933#[non_exhaustive]
934#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
935#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
936pub struct UidRange {
937    /// First UID in this range (RFC 3501 Section 9 / RFC 4315 Section 2.1).
938    pub start: u32,
939    /// `None` means a single UID (not a range) (RFC 3501 Section 9 / RFC 4315 Section 2.1).
940    pub end: Option<u32>,
941}
942
943impl UidRange {
944    /// Create a single-UID range.
945    ///
946    /// # Panics (debug builds only)
947    /// Panics if `uid` is 0 — UIDs are `nz-number` per RFC 3501 Section 9.
948    pub const fn single(uid: u32) -> Self {
949        debug_assert!(
950            uid != 0,
951            "UID must be non-zero (RFC 3501 Section 9: uniqueid = nz-number)"
952        );
953        Self {
954            start: uid,
955            end: None,
956        }
957    }
958
959    /// Create an inclusive UID range.
960    ///
961    /// # Panics (debug builds only)
962    /// Panics if `start` or `end` is 0 — UIDs are `nz-number` per RFC 3501 Section 9.
963    pub const fn range(start: u32, end: u32) -> Self {
964        debug_assert!(
965            start != 0,
966            "UID start must be non-zero (RFC 3501 Section 9: uniqueid = nz-number)"
967        );
968        debug_assert!(
969            end != 0,
970            "UID end must be non-zero (RFC 3501 Section 9: uniqueid = nz-number)"
971        );
972        Self {
973            start,
974            end: Some(end),
975        }
976    }
977
978    /// Try to create a single-UID range, returning `None` if `uid` is 0
979    /// (RFC 3501 Section 9: uniqueid = nz-number).
980    pub const fn try_single(uid: u32) -> Option<Self> {
981        if uid == 0 {
982            None
983        } else {
984            Some(Self {
985                start: uid,
986                end: None,
987            })
988        }
989    }
990
991    /// Try to create an inclusive UID range, returning `None` if `start` or `end` is 0
992    /// (RFC 3501 Section 9: uniqueid = nz-number).
993    pub const fn try_range(start: u32, end: u32) -> Option<Self> {
994        if start == 0 || end == 0 {
995            None
996        } else {
997            Some(Self {
998                start,
999                end: Some(end),
1000            })
1001        }
1002    }
1003}
1004
1005/// Result of an EXPUNGE command (RFC 3501 Section 7.4.1 / RFC 7162 Section 3.2.10).
1006///
1007/// When QRESYNC is enabled (RFC 7162 Section 3.2.3), the server sends
1008/// `VANISHED` responses instead of `EXPUNGE`. This enum allows callers
1009/// to handle both cases.
1010#[non_exhaustive]
1011#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1012#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1013pub enum ExpungeResult {
1014    /// Classic EXPUNGE — sequence numbers of removed messages (RFC 3501 Section 7.4.1).
1015    ///
1016    /// Returned when QRESYNC is NOT enabled.
1017    Expunged(Vec<u32>),
1018    /// VANISHED — UID ranges of removed messages (RFC 7162 Section 3.2.10).
1019    ///
1020    /// Returned when QRESYNC IS enabled. The server sends VANISHED
1021    /// instead of EXPUNGE after `ENABLE QRESYNC`.
1022    Vanished(Vec<UidRange>),
1023}
1024
1025impl Default for ExpungeResult {
1026    fn default() -> Self {
1027        Self::Expunged(Vec::new())
1028    }
1029}
1030
1031/// Result of a MOVE command (RFC 6851 Section 3).
1032///
1033/// RFC 6851 Section 3 specifies that the server sends EXPUNGE (or VANISHED
1034/// when QRESYNC is enabled per RFC 7162 Section 3.2.10) responses *before*
1035/// the tagged OK, followed by a COPYUID response code in the tagged OK
1036/// (RFC 6851 Section 4.3). This struct captures both pieces of information.
1037#[non_exhaustive]
1038#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
1039#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1040pub struct MoveResult {
1041    /// The response code from the tagged OK, typically `COPYUID`
1042    /// (RFC 6851 Section 4.3).
1043    pub code: Option<ResponseCode>,
1044    /// The EXPUNGE or VANISHED responses that preceded the tagged OK
1045    /// (RFC 6851 Section 3 / RFC 7162 Section 3.2.10).
1046    pub expunged: ExpungeResult,
1047}
1048
1049/// Result of a COPY or UID COPY command (RFC 3501 Section 6.4.7).
1050///
1051/// RFC 4315 Section 3 specifies that the server SHOULD respond with a
1052/// `[COPYUID uid-validity source-uids dest-uids]` response code in the
1053/// tagged OK. This struct captures that response code.
1054#[non_exhaustive]
1055#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
1056#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1057pub struct CopyResult {
1058    /// The response code from the tagged OK, typically `COPYUID`
1059    /// (RFC 4315 Section 3). `None` when the server omits the response code.
1060    pub code: Option<ResponseCode>,
1061}
1062
1063/// Parameters for QRESYNC-enabled SELECT/EXAMINE (RFC 7162 Section 3.2.5.2).
1064///
1065/// Allows the client to provide its last known state so the server can send
1066/// only the changes since the last synchronization point.
1067#[non_exhaustive]
1068#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
1069#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1070pub struct QresyncParams {
1071    /// The UIDVALIDITY value from the last session (RFC 7162 Section 3.2.5.2).
1072    pub uid_validity: u32,
1073    /// The highest MODSEQ value the client has cached (RFC 7162 Section 3.2.5.2).
1074    pub mod_seq: u64,
1075    /// Optional set of known UIDs for more efficient resync (RFC 7162 Section 3.2.5.2).
1076    pub known_uids: Option<String>,
1077    /// Optional sequence-to-UID mapping for detecting message renumbering
1078    /// (RFC 7162 Section 3.2.5.2).
1079    ///
1080    /// `seq-match-data = "(" known-sequence-set SP known-uid-set ")"`
1081    pub seq_match_data: Option<(String, String)>,
1082}
1083
1084impl QresyncParams {
1085    /// Create QRESYNC parameters with the required fields
1086    /// (RFC 7162 Section 3.2.5.2).
1087    ///
1088    /// Optional fields (`known_uids`, `seq_match_data`) default to `None`.
1089    pub fn new(uid_validity: u32, mod_seq: u64) -> Self {
1090        Self {
1091            uid_validity,
1092            mod_seq,
1093            known_uids: None,
1094            seq_match_data: None,
1095        }
1096    }
1097}
1098
1099/// Options for SELECT/EXAMINE commands beyond the basic mailbox name
1100/// (RFC 3501 Sections 6.3.1/6.3.2, RFC 7162 Sections 3.1.8 and 3.2.5.2).
1101///
1102/// Used with [`ImapConnection::select_with`] and [`ImapConnection::examine_with`]
1103/// to request CONDSTORE or QRESYNC extensions without requiring separate method
1104/// variants for each combination.
1105#[non_exhaustive]
1106#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
1107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1108pub struct SelectOptions {
1109    /// Enable CONDSTORE per-message mod-sequence tracking
1110    /// (RFC 7162 Section 3.1.1).
1111    ///
1112    /// When `true`, the server includes `HIGHESTMODSEQ` in the OK response
1113    /// and tracks per-message MODSEQ values for the selected mailbox.
1114    /// Requires the CONDSTORE or QRESYNC capability (RFC 7162 Section 3.1).
1115    pub condstore: bool,
1116    /// QRESYNC parameters for efficient delta sync
1117    /// (RFC 7162 Section 3.2.5.2).
1118    ///
1119    /// Provides the server with the client's last known UIDVALIDITY and MODSEQ
1120    /// so it can send `VANISHED (EARLIER)` and `FETCH (FLAGS)` for changed
1121    /// messages instead of a full resync.
1122    /// Requires the QRESYNC capability to have been enabled first
1123    /// (RFC 7162 Section 3.2.5).
1124    pub qresync: Option<QresyncParams>,
1125}
1126
1127impl SelectOptions {
1128    /// Create options with CONDSTORE enabled (RFC 7162 Section 3.1.1).
1129    pub fn condstore() -> Self {
1130        Self {
1131            condstore: true,
1132            ..Self::default()
1133        }
1134    }
1135
1136    /// Create options with QRESYNC parameters (RFC 7162 Section 3.2.5.2).
1137    pub fn qresync(params: QresyncParams) -> Self {
1138        Self {
1139            qresync: Some(params),
1140            ..Self::default()
1141        }
1142    }
1143}
1144
1145/// A single quota resource from a QUOTA response (RFC 2087 Section 5.1).
1146///
1147/// Each resource triplet consists of a name (e.g. `STORAGE`, `MESSAGE`),
1148/// the current usage, and the limit.
1149#[non_exhaustive]
1150#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
1151#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1152pub struct QuotaResource {
1153    /// Resource name (e.g. `"STORAGE"`, `"MESSAGE"`) (RFC 2087 Section 5.1).
1154    pub name: String,
1155    /// Current usage of this resource (RFC 2087 Section 5.1).
1156    pub usage: u64,
1157    /// Server-imposed limit for this resource (RFC 2087 Section 5.1).
1158    pub limit: u64,
1159}
1160
1161/// A single ACL entry from an ACL response (RFC 4314 Section 3.6).
1162///
1163/// Each entry pairs an identifier (user or group name) with a rights string.
1164#[non_exhaustive]
1165#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
1166#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1167pub struct AclEntry {
1168    /// The identifier (user or group) this entry applies to (RFC 4314 Section 3.6).
1169    pub identifier: String,
1170    /// The rights string for this identifier (RFC 4314 Section 3.6).
1171    pub rights: String,
1172}
1173
1174/// A single metadata entry from a METADATA response (RFC 5464 Section 4.4).
1175///
1176/// Each entry has a name (e.g. `/private/comment`) and an optional value.
1177/// A `None` value indicates the entry does not exist or has been deleted.
1178///
1179/// RFC 5464 Section 5 formal syntax: `value = nstring / literal8`.
1180/// The `literal8` form (`~{n}\r\n<data>`) allows arbitrary binary octets,
1181/// so the value is stored as raw bytes rather than a UTF-8 string.
1182#[non_exhaustive]
1183#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
1184#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1185pub struct MetadataEntry {
1186    /// Entry name (e.g. `/private/comment`, `/shared/vendor/foo`) (RFC 5464 Section 3.2).
1187    pub name: String,
1188    /// Entry value as raw bytes, or `None` if the entry does not exist (RFC 5464 Section 4.4).
1189    ///
1190    /// RFC 5464 Section 5: `value = nstring / literal8` — values may contain
1191    /// arbitrary binary data via the `literal8` syntax, so `Vec<u8>` is used
1192    /// instead of `String` to preserve binary fidelity.
1193    pub value: Option<Vec<u8>>,
1194}
1195
1196/// Result of a `GETMETADATA` command (RFC 5464 Section 4.2).
1197///
1198/// When NOTIFY metadata is active (RFC 5465 Sections 5.6–5.8), the protocol
1199/// provides no marker to distinguish solicited `METADATA` responses from
1200/// unsolicited NOTIFY `METADATA` for the same mailbox — they are
1201/// wire-identical.  Unlike `STATUS` (which expects exactly one solicited
1202/// response, enabling a last-match heuristic), `GETMETADATA` can legitimately
1203/// produce multiple solicited `METADATA` response lines, so no reliable
1204/// heuristic exists.
1205///
1206/// This struct exposes the ambiguity via [`notify_ambiguity`](Self::notify_ambiguity)
1207/// so callers can take appropriate action (e.g. treat entries as potentially
1208/// stale, re-query without NOTIFY, or duplicate entries to a NOTIFY event
1209/// pipeline).
1210#[non_exhaustive]
1211#[derive(Debug, Clone, PartialEq, Eq)]
1212#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1213pub struct MetadataResult {
1214    /// All metadata entries from matching `METADATA` responses, merged.
1215    pub entries: Vec<MetadataEntry>,
1216    /// `true` when NOTIFY metadata was active during the call (RFC 5465
1217    /// Sections 5.6–5.8), meaning some entries may be from unsolicited
1218    /// NOTIFY events that were indistinguishable from the solicited response.
1219    ///
1220    /// When `true`, callers that require unambiguous results should avoid
1221    /// issuing `GETMETADATA` while NOTIFY metadata delivery is active, or
1222    /// treat all returned entries as potentially including interleaved
1223    /// notifications.
1224    pub notify_ambiguity: bool,
1225}
1226
1227/// A node in a THREAD response tree (RFC 5256 Section 4).
1228///
1229/// Each node represents a message in a thread. A dummy parent (`id == None`)
1230/// is used when the threading algorithm infers a parent that does not
1231/// correspond to an existing message.
1232#[non_exhaustive]
1233#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
1234#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1235pub struct ThreadNode {
1236    /// UID (or sequence number) of this message, or `None` if this is a
1237    /// dummy parent (RFC 5256 Section 4).
1238    pub id: Option<u32>,
1239    /// Child thread nodes (RFC 5256 Section 4).
1240    pub children: Vec<Self>,
1241}
1242
1243#[cfg(test)]
1244#[path = "response_tests.rs"]
1245mod tests;