daaki_imap/connection/extensions.rs
1#![allow(clippy::wildcard_imports)]
2use super::*;
3
4impl ImapConnection {
5 // -----------------------------------------------------------------------
6 // COMPRESS (RFC 4978)
7 // -----------------------------------------------------------------------
8
9 /// COMPRESS DEFLATE — negotiate and activate compression (RFC 4978 Section 4).
10 ///
11 /// Sends the `COMPRESS DEFLATE` command. If the server accepts, wraps the
12 /// current transport stream in a deflate compression layer using raw deflate
13 /// (RFC 1951) as required by RFC 4978 Section 3.
14 ///
15 /// After successful return, all subsequent I/O on this connection is
16 /// compressed. Any already-buffered post-OK compressed bytes are preserved
17 /// so the first compressed server response is not lost.
18 ///
19 /// Requires the `COMPRESS=DEFLATE` capability. RFC 4978 Section 3 permits
20 /// COMPRESS to be negotiated either before or after TLS; the effective
21 /// on-the-wire layering is still compression before encryption.
22 ///
23 /// The upgrade is atomic via the `Poisoned` sentinel pattern (I9, I10)
24 /// — handled entirely by the driver task.
25 pub async fn compress(&self, timeout: Duration) -> Result<(), Error> {
26 // RFC 4978 Section 4: COMPRESS is valid in Authenticated and Selected states.
27 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
28
29 // Check COMPRESS=DEFLATE capability from the snapshot.
30 {
31 let snap = self.state_rx.borrow();
32 if !snap.capabilities.contains(&Capability::CompressDeflate) {
33 return Err(Error::MissingCapability("COMPRESS=DEFLATE".into()));
34 }
35 }
36
37 tokio::time::timeout(
38 timeout,
39 self.submit_upgrade(driver::UpgradePayload::Compress),
40 )
41 .await
42 .map_err(|_| Error::Timeout)?
43 }
44
45 // -----------------------------------------------------------------------
46 // NOTIFY (RFC 5465)
47 // -----------------------------------------------------------------------
48
49 /// NOTIFY SET — register interest in mailbox and message events
50 /// (RFC 5465 Section 3).
51 ///
52 /// Replaces any previous NOTIFY configuration. A successful NOTIFY SET
53 /// has an implicit NOOP effect: the server flushes any pending changes
54 /// to the selected mailbox before the tagged OK (RFC 5465 Section 3).
55 ///
56 /// Notifications arrive as typed events on the event queue and can be
57 /// retrieved via the event receiver.
58 ///
59 /// # Errors
60 ///
61 /// - [`Error::MissingCapability`] if the server does not advertise `NOTIFY`.
62 /// - [`Error::Protocol`] if the command is issued in an invalid state.
63 /// - [`Error::No`] if the server rejects the request (e.g. `[BADEVENT]`
64 /// for unsupported event types, RFC 5465 Section 5).
65 pub async fn notify_set(
66 &self,
67 params: NotifySetParams,
68 timeout: Duration,
69 ) -> Result<(), Error> {
70 // RFC 5465 Section 3: NOTIFY is a `command-auth` extension, valid
71 // in Authenticated or Selected state.
72 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
73 self.require_notify()?;
74
75 // RFC 6855 Section 6: UTF8=ONLY servers reject commands that might
76 // require UTF-8 support until ENABLE UTF8=ACCEPT. Only check when
77 // the command actually carries mailbox strings (Subtree/Mailboxes
78 // filters). Atom-only registrations like `(selected (MessageNew))`
79 // contain no mailbox strings and are valid without ENABLE.
80 let has_mailbox_strings = params.event_groups.iter().any(|g| {
81 matches!(
82 g.filter,
83 MailboxFilter::Subtree(_) | MailboxFilter::Mailboxes(_)
84 )
85 });
86 if has_mailbox_strings {
87 self.check_utf8_only_enforced()?;
88 }
89
90 let cmd = Command::NotifySet(params);
91 let consumer = super::dispatch::NotifySetConsumer::default();
92 // Driver sets in_notify_set before sending; apply_tagged updates
93 // notify flags on tagged OK. NOTIFICATIONOVERFLOW clears them.
94 // submit_regular returns Result<Result<bool, Error>, Error>.
95 // Inner Result: consumer wraps NO/BAD as output (not finalize
96 // error) so reclassified_as_events is always emitted.
97 let overflow = tokio::time::timeout(timeout, self.submit_regular(cmd, consumer))
98 .await
99 .map_err(|_| Error::Timeout)???;
100
101 if overflow {
102 // RFC 5465 Section 5.8: server cannot keep up. Notify flags
103 // already cleared by apply_side_effects in the driver.
104 warn!(
105 "NOTIFICATIONOVERFLOW during NOTIFY SET — registration \
106 cleared (RFC 5465 Section 5.8)"
107 );
108 }
109
110 debug!("NOTIFY SET completed (RFC 5465)");
111 Ok(())
112 }
113
114 /// NOTIFY NONE — cancel all event subscriptions (RFC 5465 Section 3).
115 ///
116 /// Reverts to baseline IMAP behavior where the server only sends
117 /// notifications during command processing, and only for the selected
118 /// mailbox.
119 ///
120 /// # Errors
121 ///
122 /// - [`Error::MissingCapability`] if the server does not advertise `NOTIFY`.
123 /// - [`Error::Protocol`] if the command is issued in an invalid state.
124 pub async fn notify_none(&self, timeout: Duration) -> Result<(), Error> {
125 // RFC 5465 Section 3: NOTIFY is a `command-auth` extension.
126 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
127 self.require_notify()?;
128
129 let cmd = Command::NotifyNone;
130 // Driver sets in_notify_set(default) before sending;
131 // apply_tagged resets notify flags on tagged OK.
132 tokio::time::timeout(
133 timeout,
134 self.submit_regular(cmd, super::dispatch::TaggedOkConsumer::default()),
135 )
136 .await
137 .map_err(|_| Error::Timeout)??;
138
139 debug!("NOTIFY NONE — notifications disabled (RFC 5465)");
140 Ok(())
141 }
142
143 // -----------------------------------------------------------------------
144 // ENABLE (RFC 5161 Section 3 / RFC 9051 Section 6.3.1)
145 // -----------------------------------------------------------------------
146
147 /// ENABLE UTF8=ACCEPT — negotiate UTF-8 support (RFC 6855 Section 3).
148 ///
149 /// Convenience wrapper around [`enable`](Self::enable) that sends
150 /// `ENABLE UTF8=ACCEPT` and returns `true` if the server confirmed
151 /// the extension, `false` if the server did not include it in its
152 /// `ENABLED` response.
153 ///
154 /// Must be issued in authenticated state before SELECT/EXAMINE
155 /// (RFC 5161 Section 2).
156 pub async fn enable_utf8(&self, timeout: Duration) -> Result<bool, Error> {
157 let enabled = self.enable(&["UTF8=ACCEPT"], timeout).await?;
158 Ok(enabled
159 .iter()
160 .any(|e| e.eq_ignore_ascii_case("UTF8=ACCEPT")))
161 }
162
163 /// ENABLE — request the server to activate one or more IMAP extensions
164 /// (RFC 5161 Section 3 / RFC 9051 Section 6.3.1).
165 ///
166 /// Sends the ENABLE command with the given capability atoms and returns
167 /// the list of extensions the server actually activated (per the
168 /// untagged `ENABLED` response).
169 ///
170 /// # State requirement
171 ///
172 /// RFC 5161 Section 2: ENABLE is valid only in the Authenticated state,
173 /// before any mailbox is selected. Attempting to ENABLE in the Selected
174 /// state returns [`Error::Protocol`].
175 ///
176 /// # Ordering constraint
177 ///
178 /// RFC 5161 Section 2.2: ENABLE MUST be issued before any command that
179 /// depends on the extension (e.g. SELECT with QRESYNC requires
180 /// `ENABLE QRESYNC` first).
181 ///
182 /// # Return value
183 ///
184 /// Returns only the extensions the server activated in this call. The
185 /// server may return a subset of the requested capabilities. Already-
186 /// enabled extensions are included in the cached state but may not
187 /// appear in the per-call return value.
188 ///
189 /// # Snapshot timing
190 ///
191 /// The `enabled` list in the cached state snapshot is updated by the
192 /// driver after the tagged OK is processed.
193 pub async fn enable(
194 &self,
195 capabilities: &[&str],
196 timeout: Duration,
197 ) -> Result<Vec<String>, Error> {
198 // RFC 5161 Section 2: ENABLE is only valid in Authenticated state.
199 self.require_state(&[SessionState::Authenticated])?;
200 {
201 let snap = self.state_rx.borrow();
202 if !snap.capabilities.contains(&Capability::Enable)
203 && !super::auth::is_rev2_from_snapshot(&snap)
204 {
205 return Err(Error::MissingCapability("ENABLE".into()));
206 }
207 }
208 let cmd = Command::Enable {
209 capabilities: capabilities.iter().map(|c| (*c).to_owned()).collect(),
210 };
211 let consumer = super::dispatch::EnableConsumer::default();
212 tokio::time::timeout(timeout, self.submit_regular(cmd, consumer))
213 .await
214 .map_err(|_| Error::Timeout)?
215 }
216
217 // -----------------------------------------------------------------------
218 // NAMESPACE (RFC 2342 / RFC 9051 Section 6.3.11)
219 // -----------------------------------------------------------------------
220
221 /// NAMESPACE — query the server's namespace configuration
222 /// (RFC 2342 Section 4 / RFC 9051 Section 6.3.11).
223 ///
224 /// Returns the server's personal, other-users, and shared namespace
225 /// descriptors.
226 ///
227 /// Requires the `NAMESPACE` capability or an `IMAP4rev2` connection
228 /// (RFC 9051 folds NAMESPACE into the base protocol).
229 pub async fn namespace(&self, timeout: Duration) -> Result<NamespaceResponse, Error> {
230 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
231 {
232 let snap = self.state_rx.borrow();
233 if !snap.capabilities.contains(&Capability::Namespace)
234 && !super::auth::is_rev2_from_snapshot(&snap)
235 {
236 return Err(Error::MissingCapability("NAMESPACE".into()));
237 }
238 }
239 tokio::time::timeout(
240 timeout,
241 self.submit_regular(
242 Command::Namespace,
243 super::dispatch::NamespaceConsumer::default(),
244 ),
245 )
246 .await
247 .map_err(|_| Error::Timeout)?
248 }
249
250 // -----------------------------------------------------------------------
251 // ID (RFC 2971)
252 // -----------------------------------------------------------------------
253
254 /// ID — exchange client/server identification (RFC 2971 Section 3.1).
255 ///
256 /// Sends client identification parameters to the server and returns
257 /// the server's identification parameters. Each parameter is a
258 /// `(field_name, value)` pair; a `None` value encodes as `NIL` on the
259 /// wire (RFC 2971 Section 3.1).
260 ///
261 /// Valid in any state (RFC 2971 Section 3.1).
262 pub async fn id(
263 &self,
264 params: &[(&str, Option<&str>)],
265 timeout: Duration,
266 ) -> Result<Vec<(String, Option<String>)>, Error> {
267 {
268 let snap = self.state_rx.borrow();
269 if !snap.capabilities.contains(&Capability::Id) {
270 return Err(Error::MissingCapability("ID".into()));
271 }
272 }
273 let cmd = Command::Id(
274 params
275 .iter()
276 .map(|(k, v)| ((*k).to_owned(), v.map(str::to_owned)))
277 .collect(),
278 );
279 tokio::time::timeout(
280 timeout,
281 self.submit_regular(cmd, super::dispatch::IdConsumer::default()),
282 )
283 .await
284 .map_err(|_| Error::Timeout)?
285 }
286
287 // -----------------------------------------------------------------------
288 // GETMETADATA / SETMETADATA (RFC 5464)
289 // -----------------------------------------------------------------------
290
291 /// GETMETADATA — retrieve mailbox or server metadata
292 /// (RFC 5464 Section 4.2).
293 ///
294 /// Fetches metadata entries for the given mailbox. Use an empty string
295 /// `""` for server-level metadata (RFC 5464 Section 4.2).
296 ///
297 /// `max_size` limits the size of returned values in bytes
298 /// (RFC 5464 Section 4.2.2). `depth` controls entry hierarchy
299 /// traversal: `"0"` (default), `"1"`, or `"infinity"`
300 /// (RFC 5464 Section 4.2.2).
301 ///
302 /// Requires `METADATA` or `METADATA-SERVER` capability
303 /// (RFC 5464 Section 1).
304 pub async fn get_metadata(
305 &self,
306 mailbox: &str,
307 entries: &[&str],
308 max_size: Option<u64>,
309 depth: Option<&str>,
310 timeout: Duration,
311 ) -> Result<MetadataResult, Error> {
312 self.check_utf8_only_enforced()?;
313 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
314 {
315 let snap = self.state_rx.borrow();
316 if !snap.capabilities.contains(&Capability::Metadata)
317 && !snap.capabilities.contains(&Capability::MetadataServer)
318 {
319 return Err(Error::MissingCapability("METADATA".into()));
320 }
321 }
322 let mailbox_name = MailboxName::new(mailbox)?;
323 let cmd = Command::GetMetadata {
324 mailbox: mailbox_name.clone(),
325 entries: entries.iter().map(|e| (*e).to_owned()).collect(),
326 max_size,
327 depth: depth.map(str::to_owned),
328 };
329 let consumer = super::dispatch::MetadataConsumer::new(mailbox_name.as_str().to_owned());
330 tokio::time::timeout(timeout, self.submit_regular(cmd, consumer))
331 .await
332 .map_err(|_| Error::Timeout)?
333 }
334
335 /// SETMETADATA — set or delete mailbox/server metadata entries
336 /// (RFC 5464 Section 4.3).
337 ///
338 /// Each entry is a `(name, value)` pair. A `None` value deletes the
339 /// entry (RFC 5464 Section 4.3).
340 ///
341 /// Requires `METADATA` or `METADATA-SERVER` capability
342 /// (RFC 5464 Section 1).
343 pub async fn set_metadata(
344 &self,
345 mailbox: &str,
346 entries: &[(&str, Option<&[u8]>)],
347 timeout: Duration,
348 ) -> Result<(), Error> {
349 self.check_utf8_only_enforced()?;
350 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
351 {
352 let snap = self.state_rx.borrow();
353 if !snap.capabilities.contains(&Capability::Metadata)
354 && !snap.capabilities.contains(&Capability::MetadataServer)
355 {
356 return Err(Error::MissingCapability("METADATA".into()));
357 }
358 }
359 let mailbox_name = MailboxName::new(mailbox)?;
360 let cmd = Command::SetMetadata {
361 mailbox: mailbox_name,
362 entries: entries
363 .iter()
364 .map(|(k, v)| ((*k).to_owned(), v.map(<[u8]>::to_vec)))
365 .collect(),
366 };
367 tokio::time::timeout(
368 timeout,
369 self.submit_regular(cmd, super::dispatch::TaggedOkConsumer::default()),
370 )
371 .await
372 .map_err(|_| Error::Timeout)?
373 }
374
375 // -----------------------------------------------------------------------
376 // QUOTA (RFC 2087 / RFC 9208)
377 // -----------------------------------------------------------------------
378
379 /// GETQUOTA — query quota resources for a quota root
380 /// (RFC 2087 Section 4.2 / RFC 9208 Section 4.2).
381 ///
382 /// Returns the resource limits and usage for the specified quota root.
383 ///
384 /// Requires the `QUOTA` capability (RFC 2087 Section 5.1).
385 pub async fn get_quota(
386 &self,
387 root: &str,
388 timeout: Duration,
389 ) -> Result<Vec<QuotaResource>, Error> {
390 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
391 {
392 let snap = self.state_rx.borrow();
393 if !snap.capabilities.contains(&Capability::Quota) {
394 return Err(Error::MissingCapability("QUOTA".into()));
395 }
396 }
397 let cmd = Command::GetQuota {
398 root: root.to_owned(),
399 };
400 let consumer = super::dispatch::QuotaConsumer::new(root.to_owned());
401 tokio::time::timeout(timeout, self.submit_regular(cmd, consumer))
402 .await
403 .map_err(|_| Error::Timeout)?
404 }
405
406 /// GETQUOTAROOT — query quota roots for a mailbox
407 /// (RFC 2087 Section 4.3 / RFC 9208 Section 4.3).
408 ///
409 /// Returns the quota root names and their associated quota resources
410 /// for the given mailbox.
411 ///
412 /// Requires the `QUOTA` capability (RFC 2087 Section 5.1).
413 pub async fn get_quota_root(
414 &self,
415 mailbox: &str,
416 timeout: Duration,
417 ) -> Result<QuotaRootResponse, Error> {
418 self.check_utf8_only_enforced()?;
419 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
420 {
421 let snap = self.state_rx.borrow();
422 if !snap.capabilities.contains(&Capability::Quota) {
423 return Err(Error::MissingCapability("QUOTA".into()));
424 }
425 }
426 let mailbox_name = MailboxName::new(mailbox)?;
427 let cmd = Command::GetQuotaRoot {
428 mailbox: mailbox_name.clone(),
429 };
430 let consumer = super::dispatch::QuotaRootConsumer::new(mailbox_name.as_str().to_owned());
431 tokio::time::timeout(timeout, self.submit_regular(cmd, consumer))
432 .await
433 .map_err(|_| Error::Timeout)?
434 }
435
436 /// SETQUOTA — set resource limits on a quota root
437 /// (RFC 2087 Section 4.1 / RFC 9208 Section 4.1).
438 ///
439 /// Each element of `resources` is a `(resource_name, limit)` pair —
440 /// e.g. `("STORAGE", 51200)`.
441 ///
442 /// Requires the `QUOTASET` capability (RFC 9208 Section 3.2).
443 pub async fn set_quota(
444 &self,
445 root: &str,
446 resources: &[(&str, u64)],
447 timeout: Duration,
448 ) -> Result<Vec<QuotaResource>, Error> {
449 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
450 {
451 let snap = self.state_rx.borrow();
452 if !snap.capabilities.contains(&Capability::QuotaSet) {
453 return Err(Error::MissingCapability("QUOTASET".into()));
454 }
455 }
456 let cmd = Command::SetQuota {
457 root: root.to_owned(),
458 resources: resources
459 .iter()
460 .map(|(name, limit)| ((*name).to_owned(), *limit))
461 .collect(),
462 };
463 let consumer = super::dispatch::QuotaConsumer::new(root.to_owned());
464 tokio::time::timeout(timeout, self.submit_regular(cmd, consumer))
465 .await
466 .map_err(|_| Error::Timeout)?
467 }
468
469 // -----------------------------------------------------------------------
470 // ACL (RFC 4314)
471 // -----------------------------------------------------------------------
472
473 /// SETACL — set access control list entries for a mailbox
474 /// (RFC 4314 Section 3.1).
475 ///
476 /// Sets the rights for `identifier` on `mailbox`. The `rights` string
477 /// uses the format defined in RFC 4314 Section 2 (e.g., `"+lrswipkxte"`
478 /// to add rights, `"-d"` to remove, or a bare string to replace).
479 ///
480 /// Requires the `ACL` capability (RFC 4314 Section 1).
481 pub async fn set_acl(
482 &self,
483 mailbox: &str,
484 identifier: &str,
485 rights: &str,
486 timeout: Duration,
487 ) -> Result<(), Error> {
488 self.check_utf8_only_enforced()?;
489 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
490 {
491 let snap = self.state_rx.borrow();
492 if !snap.capabilities.contains(&Capability::Acl) {
493 return Err(Error::MissingCapability("ACL".into()));
494 }
495 }
496 let mailbox_name = MailboxName::new(mailbox)?;
497 let cmd = Command::SetAcl {
498 mailbox: mailbox_name,
499 identifier: identifier.to_owned(),
500 rights: rights.to_owned(),
501 };
502 tokio::time::timeout(
503 timeout,
504 self.submit_regular(cmd, super::dispatch::TaggedOkConsumer::default()),
505 )
506 .await
507 .map_err(|_| Error::Timeout)?
508 }
509
510 /// DELETEACL — remove an access control list entry for a mailbox
511 /// (RFC 4314 Section 3.2).
512 ///
513 /// Removes all rights for `identifier` on `mailbox`.
514 ///
515 /// Requires the `ACL` capability (RFC 4314 Section 1).
516 pub async fn delete_acl(
517 &self,
518 mailbox: &str,
519 identifier: &str,
520 timeout: Duration,
521 ) -> Result<(), Error> {
522 self.check_utf8_only_enforced()?;
523 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
524 {
525 let snap = self.state_rx.borrow();
526 if !snap.capabilities.contains(&Capability::Acl) {
527 return Err(Error::MissingCapability("ACL".into()));
528 }
529 }
530 let mailbox_name = MailboxName::new(mailbox)?;
531 let cmd = Command::DeleteAcl {
532 mailbox: mailbox_name,
533 identifier: identifier.to_owned(),
534 };
535 tokio::time::timeout(
536 timeout,
537 self.submit_regular(cmd, super::dispatch::TaggedOkConsumer::default()),
538 )
539 .await
540 .map_err(|_| Error::Timeout)?
541 }
542
543 /// GETACL — retrieve the access control list for a mailbox
544 /// (RFC 4314 Section 3.3).
545 ///
546 /// Returns the list of identifier/rights pairs for the given mailbox.
547 ///
548 /// Requires the `ACL` capability (RFC 4314 Section 1).
549 pub async fn get_acl(&self, mailbox: &str, timeout: Duration) -> Result<Vec<AclEntry>, Error> {
550 self.check_utf8_only_enforced()?;
551 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
552 {
553 let snap = self.state_rx.borrow();
554 if !snap.capabilities.contains(&Capability::Acl) {
555 return Err(Error::MissingCapability("ACL".into()));
556 }
557 }
558 let mailbox_name = MailboxName::new(mailbox)?;
559 let cmd = Command::GetAcl {
560 mailbox: mailbox_name.clone(),
561 };
562 let consumer = super::dispatch::AclConsumer::new(mailbox_name.as_str().to_owned());
563 tokio::time::timeout(timeout, self.submit_regular(cmd, consumer))
564 .await
565 .map_err(|_| Error::Timeout)?
566 }
567
568 /// LISTRIGHTS — query the set of rights grantable to an identifier
569 /// on a mailbox (RFC 4314 Section 3.4).
570 ///
571 /// Returns the required (always-granted) rights and the groups of
572 /// optional rights that can be independently granted or revoked.
573 ///
574 /// Requires the `ACL` capability (RFC 4314 Section 1).
575 pub async fn list_rights(
576 &self,
577 mailbox: &str,
578 identifier: &str,
579 timeout: Duration,
580 ) -> Result<ListRightsResponse, Error> {
581 self.check_utf8_only_enforced()?;
582 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
583 {
584 let snap = self.state_rx.borrow();
585 if !snap.capabilities.contains(&Capability::Acl) {
586 return Err(Error::MissingCapability("ACL".into()));
587 }
588 }
589 let mailbox_name = MailboxName::new(mailbox)?;
590 let cmd = Command::ListRights {
591 mailbox: mailbox_name.clone(),
592 identifier: identifier.to_owned(),
593 };
594 let consumer = super::dispatch::ListRightsConsumer::new(
595 mailbox_name.as_str().to_owned(),
596 identifier.to_owned(),
597 );
598 tokio::time::timeout(timeout, self.submit_regular(cmd, consumer))
599 .await
600 .map_err(|_| Error::Timeout)?
601 }
602
603 /// MYRIGHTS — query the logged-in user's rights on a mailbox
604 /// (RFC 4314 Section 3.5).
605 ///
606 /// Returns the rights string for the current user on the given mailbox.
607 ///
608 /// Requires the `ACL` capability (RFC 4314 Section 1).
609 pub async fn my_rights(&self, mailbox: &str, timeout: Duration) -> Result<String, Error> {
610 self.check_utf8_only_enforced()?;
611 self.require_state(&[SessionState::Authenticated, SessionState::Selected])?;
612 {
613 let snap = self.state_rx.borrow();
614 if !snap.capabilities.contains(&Capability::Acl) {
615 return Err(Error::MissingCapability("ACL".into()));
616 }
617 }
618 let mailbox_name = MailboxName::new(mailbox)?;
619 let cmd = Command::MyRights {
620 mailbox: mailbox_name.clone(),
621 };
622 let consumer = super::dispatch::MyRightsConsumer::new(mailbox_name.as_str().to_owned());
623 tokio::time::timeout(timeout, self.submit_regular(cmd, consumer))
624 .await
625 .map_err(|_| Error::Timeout)?
626 }
627
628 /// Verify that the server advertises the NOTIFY capability (RFC 5465).
629 fn require_notify(&self) -> Result<(), Error> {
630 let has_notify = self
631 .state_rx
632 .borrow()
633 .capabilities
634 .contains(&Capability::Notify);
635 if !has_notify {
636 return Err(Error::MissingCapability("NOTIFY".into()));
637 }
638 Ok(())
639 }
640}
641
642/// Compute which ongoing response types a NOTIFY registration can produce.
643///
644/// Returns `(list, status, metadata)` booleans for *ongoing* notifications:
645/// - `list`: any non-selected events are registered. `MailboxName`
646/// (RFC 5465 Section 5.4) and `SubscriptionChange` (RFC 5465 Section 5.5)
647/// produce explicit LIST responses, but ALL non-selected registrations
648/// can trigger LIST responses for ACL changes — RFC 5465 Section 5.9
649/// requires `LIST \NoAccess` / `LIST` when the logged-in user loses or
650/// regains the `l` (lookup) ACL right on any monitored mailbox,
651/// regardless of which event types were requested.
652/// - `status`: message events on non-selected mailboxes — `FlagChange`
653/// and `AnnotationChange` (RFC 5465 Section 5.1), `MessageNew`
654/// (RFC 5465 Section 5.2), and `MessageExpunge` (RFC 5465 Section 5.3),
655/// all delivered as STATUS responses.
656/// - `metadata`: `MailboxMetadataChange` (RFC 5465 Section 5.6) or
657/// `ServerMetadataChange` (RFC 5465 Section 5.7) events registered —
658/// delivered as METADATA.
659///
660/// The `params.status` indicator is NOT included here — it only triggers
661/// an initial STATUS snapshot for non-selected mailboxes with message
662/// events (RFC 5465 Section 4), not ongoing STATUS notifications. Those
663/// initial responses are handled by the re-push logic in `notify_set()`.
664pub(crate) fn compute_notify_flags(params: &NotifySetParams) -> (bool, bool, bool) {
665 let mut list = false;
666 let mut status = false;
667 let mut metadata = false;
668
669 for group in ¶ms.event_groups {
670 let is_non_selected = !matches!(
671 group.filter,
672 MailboxFilter::Selected | MailboxFilter::SelectedDelayed
673 );
674
675 // RFC 5465 Section 5.9: any non-selected registration can trigger
676 // LIST \NoAccess or LIST responses for ACL changes on monitored
677 // mailboxes. Enable LIST buffering for all non-selected groups.
678 if is_non_selected && !group.events.is_empty() {
679 list = true;
680 }
681
682 for event in &group.events {
683 match event {
684 NotifyEvent::MessageNew { .. }
685 | NotifyEvent::MessageExpunge
686 | NotifyEvent::FlagChange
687 | NotifyEvent::AnnotationChange => {
688 // RFC 5465 Sections 5.1–5.3: on non-selected mailboxes
689 // all message events are delivered as STATUS responses.
690 if is_non_selected {
691 status = true;
692 }
693 }
694 NotifyEvent::MailboxMetadataChange | NotifyEvent::ServerMetadataChange => {
695 // RFC 5465 Sections 5.6–5.7: delivered as METADATA.
696 metadata = true;
697 }
698 NotifyEvent::Other(_) => {
699 // Unknown extension event — we don't know which
700 // response type the server will use for delivery.
701 // Enable ALL known buffering flags defensively so
702 // the notification is not silently dropped regardless
703 // of delivery mechanism. Note: Other(_) can only
704 // appear under non-selected filters — the encoder
705 // rejects it for selected / selected-delayed per
706 // RFC 5465 Section 6.1 / Section 8 (event-ext is a
707 // separate ABNF production from message-event).
708 // Truly extension-defined responses
709 // (`UntaggedResponse::Unknown`) are also routed
710 // to the unsolicited buffer by the dispatcher.
711 list = true;
712 status = true;
713 metadata = true;
714 }
715 _ => {}
716 }
717 }
718 }
719
720 (list, status, metadata)
721}