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}