Skip to main content

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 &params.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}