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;