Skip to main content

daaki_imap/connection/
mailbox.rs

1#![allow(clippy::wildcard_imports)]
2use super::*;
3
4impl ImapConnection {
5    // -----------------------------------------------------------------------
6    // Mailbox operations
7    // -----------------------------------------------------------------------
8
9    /// LIST mailboxes (RFC 3501 Section 6.3.8).
10    ///
11    /// For LIST-EXTENDED selection or return options, use
12    /// [`ImapConnection::list_extended`].
13    pub async fn list(
14        &self,
15        reference: &str,
16        pattern: &str,
17        timeout: Duration,
18    ) -> Result<Vec<MailboxInfo>, Error> {
19        use super::dispatch::ListConsumer;
20
21        self.check_utf8_only_enforced()?;
22        self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
23        let cmd = Command::List {
24            reference: reference.to_owned(),
25            pattern: pattern.to_owned(),
26        };
27        // The ListConsumer handles NOTIFY marker classification via the
28        // per-response notify_snapshot passed by the dispatcher. Mid-stream
29        // NOTIFICATIONOVERFLOW (RFC 5465 §5.8) is handled automatically:
30        // apply_side_effects clears the notify flags, so subsequent
31        // snapshots have list=false.
32        let inner = tokio::time::timeout(timeout, self.submit_regular(cmd, ListConsumer::new()))
33            .await
34            .map_err(|_| Error::Timeout)??;
35        inner
36    }
37
38    /// LIST mailboxes with RFC 5258 selection options, multiple patterns, and
39    /// return options (RFC 5258 Section 3 / RFC 9051 Section 6.3.9).
40    ///
41    /// Examples:
42    /// - `selection_options = &["SUBSCRIBED"]`
43    /// - `return_options = &["CHILDREN"]`
44    /// - `return_options = &["STATUS (MESSAGES UNSEEN)"]`
45    ///
46    /// On `IMAP4rev1`, the connection enforces capability gates for the
47    /// requested options:
48    /// - `LIST-EXTENDED` for RFC 5258 syntax such as selection options,
49    ///   multiple patterns, and all return options, including extension forms
50    ///   like `SPECIAL-USE` and `STATUS (...)`
51    /// - `LIST-STATUS` for `STATUS (...)`
52    /// - `SPECIAL-USE` for `SPECIAL-USE`
53    pub async fn list_extended(
54        &self,
55        reference: &str,
56        patterns: &[&str],
57        selection_options: &[&str],
58        return_options: &[&str],
59        timeout: Duration,
60    ) -> Result<Vec<MailboxInfo>, Error> {
61        use super::dispatch::ListExtendedConsumer;
62
63        self.check_utf8_only_enforced()?;
64        self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
65        self.validate_list_extended_request(patterns, selection_options, return_options)?;
66
67        if selection_options.is_empty() && return_options.is_empty() && patterns.len() == 1 {
68            return self.list(reference, patterns[0], timeout).await;
69        }
70
71        let cmd = Command::ListExtended {
72            selection_options: selection_options
73                .iter()
74                .map(|option| (*option).to_owned())
75                .collect(),
76            reference: reference.to_owned(),
77            patterns: patterns
78                .iter()
79                .map(|pattern| (*pattern).to_owned())
80                .collect(),
81            return_options: return_options
82                .iter()
83                .map(|option| (*option).to_owned())
84                .collect(),
85        };
86        // RFC 5258 Section 3: with SUBSCRIBED, the server may return
87        // subscribed-but-deleted mailboxes with \NonExistent and
88        // subscribed-but-inaccessible ones with \NoAccess. In that
89        // context these are legitimate solicited attributes, not NOTIFY
90        // markers.
91        let filter_extended = !selection_options
92            .iter()
93            .any(|o| o.eq_ignore_ascii_case("SUBSCRIBED"));
94
95        let consumer = ListExtendedConsumer::new(
96            filter_extended,
97            selection_options.iter().map(|o| (*o).to_owned()).collect(),
98        );
99        let inner = tokio::time::timeout(timeout, self.submit_regular(cmd, consumer))
100            .await
101            .map_err(|_| Error::Timeout)??;
102        inner
103    }
104
105    /// LIST with STATUS return option (RFC 5819 Section 2).
106    ///
107    /// Returns mailbox information paired with STATUS data for each mailbox.
108    /// `status_items` is the raw status items string, e.g. `"MESSAGES UNSEEN"`.
109    /// The server returns interleaved LIST and STATUS untagged responses;
110    /// this method correlates them by mailbox name.
111    pub async fn list_status(
112        &self,
113        reference: &str,
114        pattern: &str,
115        status_items: &str,
116        timeout: Duration,
117    ) -> Result<Vec<(MailboxInfo, Vec<StatusItem>)>, Error> {
118        use super::dispatch::ListStatusConsumer;
119
120        self.check_utf8_only_enforced()?;
121        self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
122        // RFC 5819 Section 2 extends the LIST command syntax from RFC 5258
123        // Section 3, so IMAP4rev1 needs both LIST-STATUS and LIST-EXTENDED.
124        // RFC 9051 Section 6.3.9 folds LIST-STATUS into the IMAP4rev2 base.
125        {
126            let snap = self.state_rx.borrow();
127            if !auth::is_rev2_from_snapshot(&snap) {
128                if !snap.capabilities.contains(&Capability::ListStatus) {
129                    return Err(Error::MissingCapability("LIST-STATUS".into()));
130                }
131                if !snap.capabilities.contains(&Capability::ListExtended) {
132                    return Err(Error::MissingCapability("LIST-EXTENDED".into()));
133                }
134            }
135        }
136        self.validate_requested_status_items(status_items)?;
137        let cmd = Command::ListStatus {
138            reference: reference.to_owned(),
139            pattern: pattern.to_owned(),
140            status_items: status_items.to_owned(),
141        };
142        let inner =
143            tokio::time::timeout(timeout, self.submit_regular(cmd, ListStatusConsumer::new()))
144                .await
145                .map_err(|_| Error::Timeout)??;
146        inner
147    }
148
149    /// SELECT a mailbox (RFC 3501 Section 6.3.1).
150    ///
151    /// For CONDSTORE or QRESYNC options, use [`select_with`](Self::select_with).
152    pub async fn select(&self, mailbox: &str, timeout: Duration) -> Result<SelectedMailbox, Error> {
153        self.select_with(mailbox, &SelectOptions::default(), timeout)
154            .await
155    }
156
157    /// SELECT a mailbox with extension options (RFC 3501 Section 6.3.1,
158    /// RFC 7162 Sections 3.1.8 and 3.2.5.2).
159    ///
160    /// Pass [`SelectOptions::default()`] for a plain SELECT, or use the
161    /// convenience constructors:
162    /// - [`SelectOptions::condstore()`] for `SELECT <mailbox> (CONDSTORE)`
163    /// - [`SelectOptions::qresync(params)`] for `SELECT <mailbox> (QRESYNC ...)`
164    pub async fn select_with(
165        &self,
166        mailbox: &str,
167        options: &SelectOptions,
168        timeout: Duration,
169    ) -> Result<SelectedMailbox, Error> {
170        self.select_or_examine(
171            mailbox,
172            false,
173            options.condstore,
174            options.qresync.clone(),
175            timeout,
176        )
177        .await
178    }
179
180    /// EXAMINE a mailbox (read-only SELECT, RFC 3501 Section 6.3.2).
181    ///
182    /// For CONDSTORE or QRESYNC options, use [`examine_with`](Self::examine_with).
183    pub async fn examine(
184        &self,
185        mailbox: &str,
186        timeout: Duration,
187    ) -> Result<SelectedMailbox, Error> {
188        self.examine_with(mailbox, &SelectOptions::default(), timeout)
189            .await
190    }
191
192    /// EXAMINE a mailbox with extension options (RFC 3501 Section 6.3.2,
193    /// RFC 7162 Sections 3.1.8 and 3.2.5.2).
194    ///
195    /// Read-only variant of [`select_with`](Self::select_with). Pass
196    /// [`SelectOptions::default()`] for a plain EXAMINE, or use the
197    /// convenience constructors:
198    /// - [`SelectOptions::condstore()`] for `EXAMINE <mailbox> (CONDSTORE)`
199    /// - [`SelectOptions::qresync(params)`] for `EXAMINE <mailbox> (QRESYNC ...)`
200    pub async fn examine_with(
201        &self,
202        mailbox: &str,
203        options: &SelectOptions,
204        timeout: Duration,
205    ) -> Result<SelectedMailbox, Error> {
206        self.select_or_examine(
207            mailbox,
208            true,
209            options.condstore,
210            options.qresync.clone(),
211            timeout,
212        )
213        .await
214    }
215
216    /// Shared implementation for SELECT and EXAMINE commands
217    /// (RFC 3501 Sections 6.3.1–6.3.2, RFC 7162 Sections 3.1.8 and 3.2.5.2).
218    ///
219    /// Handles common validation (UTF8=ONLY enforcement, session state),
220    /// extension-specific capability checks (CONDSTORE, QRESYNC), command
221    /// construction, and dispatch via [`SelectConsumer`].
222    ///
223    /// State transitions are handled by the driver task via the `in_select`
224    /// flag in `ProtocolState::apply_tagged`:
225    /// - Tagged OK → `Selected` (RFC 3501 §6.3.1)
226    /// - Tagged NO → `Authenticated` (deselects, RFC 3501 §6.3.1)
227    /// - Tagged BAD → no change (RFC 3501 §6)
228    pub(super) async fn select_or_examine(
229        &self,
230        mailbox: &str,
231        is_examine: bool,
232        condstore: bool,
233        qresync: Option<QresyncParams>,
234        timeout: Duration,
235    ) -> Result<SelectedMailbox, Error> {
236        use super::dispatch::SelectConsumer;
237
238        self.check_utf8_only_enforced()?;
239        self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
240        if condstore {
241            self.require_condstore()?;
242        }
243        if let Some(ref params) = qresync {
244            self.validate_qresync_params(params)?;
245        }
246        let wire_mailbox = MailboxName::new(mailbox)?;
247        let cmd = if is_examine {
248            Command::Examine {
249                mailbox: wire_mailbox,
250                condstore,
251                qresync,
252            }
253        } else {
254            Command::Select {
255                mailbox: wire_mailbox,
256                condstore,
257                qresync,
258            }
259        };
260
261        let consumer = SelectConsumer::new(is_examine);
262        // Consumer::Output is Result<SelectedMailbox, Error> — the inner
263        // Result carries NO/BAD/validation errors so that the consumer can
264        // reclassify accumulated responses as events on those paths.
265        let inner = tokio::time::timeout(timeout, self.submit_regular(cmd, consumer))
266            .await
267            .map_err(|_| Error::Timeout)??;
268        // State transitions (Selected on OK, Authenticated on NO) are
269        // handled by the driver's apply_tagged via the in_select flag.
270        inner
271    }
272
273    /// Validate QRESYNC parameters before SELECT/EXAMINE (RFC 7162 Section 3.2.5.2).
274    ///
275    /// Ensures QRESYNC has been `ENABLE`d and that seq-match-data is only
276    /// present when known-uids is also present (per the ABNF in RFC 7162
277    /// Section 3.2.5.2).
278    pub(super) fn validate_qresync_params(&self, params: &QresyncParams) -> Result<(), Error> {
279        // RFC 7162 Section 3.2.3: the client MUST issue ENABLE QRESYNC
280        // before using QRESYNC parameters in SELECT/EXAMINE.
281        {
282            let snap = self.state_rx.borrow();
283            if !snap.enabled.iter().any(|e| e == "QRESYNC") {
284                return Err(Error::MissingCapability("QRESYNC (not ENABLEd)".into()));
285            }
286        }
287        // RFC 7162 Section 3.2.5.2 ABNF: seq-match-data is only valid after
288        // known-uids. Reject invalid combinations instead of fabricating data.
289        if params.seq_match_data.is_some() && params.known_uids.is_none() {
290            return Err(Error::Protocol(
291                "QRESYNC seq-match-data requires known-uids \
292                 (RFC 7162 Section 3.2.5.2)"
293                    .into(),
294            ));
295        }
296        Ok(())
297    }
298
299    /// CREATE a mailbox (RFC 3501 Section 6.3.3).
300    pub async fn create(&self, mailbox: &str, timeout: Duration) -> Result<(), Error> {
301        self.create_with_mailbox_id(mailbox, timeout)
302            .await
303            .map(|_| ())
304    }
305
306    /// CREATE a mailbox and return the server-assigned `MAILBOXID` when present
307    /// (RFC 8474 Section 4.1).
308    ///
309    /// RFC 8474 Section 4.1: a server advertising `OBJECTID` MUST include a
310    /// tagged `MAILBOXID` response code on successful CREATE. Servers without
311    /// `OBJECTID` support, or non-conformant servers, may omit it, so this
312    /// method returns `Ok(None)` in that case.
313    pub async fn create_with_mailbox_id(
314        &self,
315        mailbox: &str,
316        timeout: Duration,
317    ) -> Result<Option<String>, Error> {
318        use super::dispatch::CreateConsumer;
319
320        self.check_utf8_only_enforced()?;
321        self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
322        let cmd = Command::Create {
323            mailbox: MailboxName::new(mailbox)?,
324        };
325        tokio::time::timeout(timeout, self.submit_regular(cmd, CreateConsumer::default()))
326            .await
327            .map_err(|_| Error::Timeout)?
328    }
329
330    /// CREATE a mailbox with special-use attributes (RFC 6154 Section 3).
331    ///
332    /// RFC 6154 Section 3 / Section 6 ABNF:
333    /// `create-param =/ "USE" SP "(" [use-attr *(SP use-attr)] ")"`
334    /// where `use-attr = "\All" / "\Archive" / "\Drafts" / "\Flagged" /
335    ///                    "\Junk" / "\Sent" / "\Trash" / use-attr-ext`
336    ///
337    /// Requires the server to advertise `CREATE-SPECIAL-USE` capability.
338    /// RFC 6154 Section 3: "Clients MUST NOT use the USE parameter unless the
339    /// server advertises the CREATE-SPECIAL-USE capability."
340    pub async fn create_special_use(
341        &self,
342        mailbox: &str,
343        special_use: &[MailboxAttribute],
344        timeout: Duration,
345    ) -> Result<(), Error> {
346        self.create_special_use_with_mailbox_id(mailbox, special_use, timeout)
347            .await
348            .map(|_| ())
349    }
350
351    /// CREATE a mailbox with special-use attributes and return the server's
352    /// `MAILBOXID` when present (RFC 6154 Section 3, RFC 8474 Section 4.1).
353    pub async fn create_special_use_with_mailbox_id(
354        &self,
355        mailbox: &str,
356        special_use: &[MailboxAttribute],
357        timeout: Duration,
358    ) -> Result<Option<String>, Error> {
359        use super::dispatch::CreateConsumer;
360
361        // RFC 6855 Section 3: UTF8=ONLY requires ENABLE UTF8=ACCEPT first.
362        self.check_utf8_only_enforced()?;
363        // RFC 6154 Section 3: CREATE is a `command-auth` — valid only in
364        // Authenticated or Selected state.
365        self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
366        {
367            let snap = self.state_rx.borrow();
368            if !snap.capabilities.contains(&Capability::CreateSpecialUse) {
369                return Err(Error::MissingCapability("CREATE-SPECIAL-USE".into()));
370            }
371        }
372        // RFC 6154 Section 3: "The USE parameter MUST NOT contain any
373        // non-use-attr values." Reject base LIST attributes like \Noselect,
374        // \HasChildren, etc. before sending the command.
375        if let Some(bad) = special_use.iter().find(|a| !a.is_special_use()) {
376            return Err(Error::Protocol(format!(
377                "CREATE USE parameter contains non-special-use attribute {} \
378                 (RFC 6154 Section 3: USE MUST only contain use-attr values)",
379                bad.as_imap_str()
380            )));
381        }
382        let cmd = Command::CreateSpecialUse {
383            mailbox: MailboxName::new(mailbox)?,
384            special_use: special_use.to_vec(),
385        };
386        tokio::time::timeout(timeout, self.submit_regular(cmd, CreateConsumer::default()))
387            .await
388            .map_err(|_| Error::Timeout)?
389    }
390
391    /// DELETE a mailbox (RFC 3501 Section 6.3.4).
392    pub async fn delete(&self, mailbox: &str, timeout: Duration) -> Result<(), Error> {
393        use super::dispatch::TaggedOkConsumer;
394
395        self.check_utf8_only_enforced()?;
396        self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
397        let cmd = Command::Delete {
398            mailbox: MailboxName::new(mailbox)?,
399        };
400        tokio::time::timeout(
401            timeout,
402            self.submit_regular(cmd, TaggedOkConsumer::default()),
403        )
404        .await
405        .map_err(|_| Error::Timeout)?
406    }
407
408    /// RENAME a mailbox (RFC 3501 Section 6.3.5).
409    pub async fn rename(
410        &self,
411        mailbox: &str,
412        new_name: &str,
413        timeout: Duration,
414    ) -> Result<(), Error> {
415        use super::dispatch::TaggedOkConsumer;
416
417        self.check_utf8_only_enforced()?;
418        self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
419        let cmd = Command::Rename {
420            mailbox: MailboxName::new(mailbox)?,
421            new_name: MailboxName::new(new_name)?,
422        };
423        tokio::time::timeout(
424            timeout,
425            self.submit_regular(cmd, TaggedOkConsumer::default()),
426        )
427        .await
428        .map_err(|_| Error::Timeout)?
429    }
430
431    /// SUBSCRIBE to a mailbox (RFC 3501 Section 6.3.6).
432    ///
433    /// Adds the mailbox to the server's set of "active" or "subscribed" mailboxes
434    /// returned by LSUB.
435    pub async fn subscribe(&self, mailbox: &str, timeout: Duration) -> Result<(), Error> {
436        use super::dispatch::TaggedOkConsumer;
437
438        self.check_utf8_only_enforced()?;
439        self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
440        let cmd = Command::Subscribe {
441            mailbox: MailboxName::new(mailbox)?,
442        };
443        tokio::time::timeout(
444            timeout,
445            self.submit_regular(cmd, TaggedOkConsumer::default()),
446        )
447        .await
448        .map_err(|_| Error::Timeout)?
449    }
450
451    /// UNSUBSCRIBE from a mailbox (RFC 3501 Section 6.3.7).
452    pub async fn unsubscribe(&self, mailbox: &str, timeout: Duration) -> Result<(), Error> {
453        use super::dispatch::TaggedOkConsumer;
454
455        self.check_utf8_only_enforced()?;
456        self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
457        let cmd = Command::Unsubscribe {
458            mailbox: MailboxName::new(mailbox)?,
459        };
460        tokio::time::timeout(
461            timeout,
462            self.submit_regular(cmd, TaggedOkConsumer::default()),
463        )
464        .await
465        .map_err(|_| Error::Timeout)?
466    }
467
468    /// LSUB — list subscribed mailboxes (RFC 3501 Section 6.3.9).
469    ///
470    /// Obsoleted by LIST-EXTENDED (RFC 5258) but still required for servers
471    /// that don't support `\Subscribed` attribute in LIST.
472    pub async fn lsub(
473        &self,
474        reference: &str,
475        pattern: &str,
476        timeout: Duration,
477    ) -> Result<Vec<MailboxInfo>, Error> {
478        use super::dispatch::LsubConsumer;
479
480        self.check_utf8_only_enforced()?;
481        self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
482
483        // RFC 9051 Appendix F item 19: LSUB was deprecated in IMAP4rev2.
484        // Use LIST with \Subscribed return option instead.
485        if self.is_rev2() {
486            return Err(Error::Protocol(
487                "LSUB was deprecated in IMAP4rev2 (RFC 9051 Appendix F); \
488                 use list() with \\Subscribed attribute instead"
489                    .into(),
490            ));
491        }
492
493        let cmd = Command::Lsub {
494            reference: reference.to_owned(),
495            pattern: pattern.to_owned(),
496        };
497        tokio::time::timeout(timeout, self.submit_regular(cmd, LsubConsumer::default()))
498            .await
499            .map_err(|_| Error::Timeout)?
500    }
501
502    /// CLOSE the selected mailbox (RFC 3501 Section 6.4.2).
503    ///
504    /// Permanently removes all messages with the `\Deleted` flag and returns
505    /// to the authenticated state. Use [`unselect`](Self::unselect) to deselect
506    /// without expunging.
507    ///
508    /// State transition to `Authenticated` is handled by the driver task
509    /// via the `in_close` flag in `ProtocolState::apply_tagged`.
510    pub async fn close(&self, timeout: Duration) -> Result<(), Error> {
511        use super::dispatch::TaggedOkConsumer;
512
513        self.require_state(&[SessionState::Selected])?;
514        tokio::time::timeout(
515            timeout,
516            self.submit_regular(Command::Close, TaggedOkConsumer::default()),
517        )
518        .await
519        .map_err(|_| Error::Timeout)??;
520        Ok(())
521    }
522
523    /// UNSELECT — deselect the current mailbox without expunging
524    /// (RFC 3691 Section 3).
525    ///
526    /// Closes the currently selected mailbox and returns to the
527    /// Authenticated state, but unlike [`close`](Self::close) does NOT
528    /// permanently remove messages with the `\Deleted` flag.
529    ///
530    /// Requires the `UNSELECT` capability or an `IMAP4rev2` connection
531    /// (RFC 9051 folds UNSELECT into the base protocol).
532    ///
533    /// State transition to `Authenticated` is handled by the driver task
534    /// via the `in_close` flag in `ProtocolState::apply_tagged`.
535    ///
536    /// # Snapshot timing
537    ///
538    /// The state snapshot transitions to `Authenticated` only after the
539    /// tagged OK is received and processed by the driver.
540    pub async fn unselect(&self, timeout: Duration) -> Result<(), Error> {
541        self.require_state(&[SessionState::Selected])?;
542        {
543            let snap = self.state_rx.borrow();
544            if !snap.capabilities.contains(&Capability::Unselect)
545                && !super::auth::is_rev2_from_snapshot(&snap)
546            {
547                return Err(Error::MissingCapability("UNSELECT".into()));
548            }
549        }
550        tokio::time::timeout(
551            timeout,
552            self.submit_regular(
553                Command::Unselect,
554                super::dispatch::TaggedOkConsumer::default(),
555            ),
556        )
557        .await
558        .map_err(|_| Error::Timeout)??;
559        Ok(())
560    }
561
562    /// STATUS of a mailbox without selecting it (RFC 3501 Section 6.3.10).
563    ///
564    /// `items` may be either a raw status item list such as
565    /// `"MESSAGES UNSEEN UIDNEXT"` or an already parenthesized
566    /// `"(MESSAGES UNSEEN UIDNEXT)"` list.
567    ///
568    /// Returns a [`StatusResult`] that includes any ambiguous same-mailbox
569    /// `STATUS` responses when NOTIFY is active — see its documentation for
570    /// details on the inherent protocol ambiguity (RFC 5465 Section 4).
571    pub async fn status(
572        &self,
573        mailbox: &str,
574        items: &str,
575        timeout: Duration,
576    ) -> Result<StatusResult, Error> {
577        use super::dispatch::StatusConsumer;
578
579        self.check_utf8_only_enforced()?;
580        self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
581        self.validate_requested_status_items(items)?;
582        let cmd = Command::Status {
583            mailbox: MailboxName::new(mailbox)?,
584            items: items.to_owned(),
585        };
586        tokio::time::timeout(timeout, self.submit_regular(cmd, StatusConsumer::new()))
587            .await
588            .map_err(|_| Error::Timeout)?
589    }
590}