Skip to main content

daaki_imap/connection/
seq_ops.rs

1#![allow(clippy::wildcard_imports)]
2use super::*;
3
4impl ImapConnection {
5    // -----------------------------------------------------------------------
6    // Message operations (sequence-number variants)
7    // -----------------------------------------------------------------------
8
9    /// FETCH by sequence number (RFC 3501 Section 6.4.5).
10    ///
11    /// Prefer [`uid_fetch`](Self::uid_fetch) for stable message references.
12    pub async fn fetch(
13        &self,
14        sequence_set: &SequenceSet,
15        items: &[FetchAttr],
16        timeout: Duration,
17    ) -> Result<Vec<FetchResponse>, Error> {
18        self.validate_requested_fetch_items(items)?;
19        // RFC 5182 Section 2: `$` references saved search results and requires SEARCHRES.
20        if sequence_set.as_str().contains('$') {
21            self.require_searchres()?;
22        }
23        self.fetch_impl(
24            Command::Fetch {
25                sequence_set: sequence_set.clone(),
26                items: format_fetch_attrs(items),
27                changed_since: None,
28            },
29            None,
30            timeout,
31        )
32        .await
33    }
34
35    /// FETCH with CHANGEDSINCE modifier by sequence number (RFC 7162 Section 3.1.4).
36    ///
37    /// Returns only messages whose mod-sequence is greater than `mod_seq`.
38    /// Requires the server to support CONDSTORE (RFC 7162).
39    /// Prefer [`uid_fetch_changed_since`](Self::uid_fetch_changed_since) for stable
40    /// message references.
41    pub async fn fetch_changed_since(
42        &self,
43        sequence_set: &SequenceSet,
44        items: &[FetchAttr],
45        mod_seq: u64,
46        timeout: Duration,
47    ) -> Result<Vec<FetchResponse>, Error> {
48        self.validate_requested_fetch_items(items)?;
49        // RFC 5182 Section 2: `$` references saved search results and requires SEARCHRES.
50        if sequence_set.as_str().contains('$') {
51            self.require_searchres()?;
52        }
53        self.fetch_impl(
54            Command::Fetch {
55                sequence_set: sequence_set.clone(),
56                items: format_fetch_attrs(items),
57                changed_since: Some(mod_seq),
58            },
59            Some(mod_seq),
60            timeout,
61        )
62        .await
63    }
64
65    /// FETCH streaming by sequence number (RFC 3501 Section 6.4.5).
66    ///
67    /// Pushes each [`FetchResponse`] through the provided `tx` channel as it
68    /// arrives from the server, rather than buffering the entire result set
69    /// in memory. The channel is closed when the tagged OK is received.
70    ///
71    /// Prefer [`uid_fetch_streaming`](Self::uid_fetch_streaming) for stable
72    /// message references.
73    pub async fn fetch_streaming(
74        &self,
75        sequence_set: &SequenceSet,
76        items: &[FetchAttr],
77        tx: tokio::sync::mpsc::Sender<Result<FetchResponse, Error>>,
78        timeout: Duration,
79    ) -> Result<(), Error> {
80        self.validate_requested_fetch_items(items)?;
81        // RFC 5182 Section 2: `$` references saved search results and requires SEARCHRES.
82        if sequence_set.as_str().contains('$') {
83            self.require_searchres()?;
84        }
85        self.fetch_streaming_impl(
86            Command::Fetch {
87                sequence_set: sequence_set.clone(),
88                items: format_fetch_attrs(items),
89                changed_since: None,
90            },
91            tx,
92            timeout,
93        )
94        .await
95    }
96
97    /// SEARCH by sequence number (RFC 3501 Section 6.4.4).
98    ///
99    /// Returns a [`SearchResult`] containing matching sequence numbers and an
100    /// optional MODSEQ value (RFC 7162 Section 3.1.5).
101    /// Prefer [`uid_search`](Self::uid_search) for stable message references.
102    /// Handles both SEARCH and ESEARCH responses. For ESEARCH, the ALL uid-set
103    /// is expanded into individual sequence numbers for backward compatibility.
104    ///
105    /// Accepts anything that implements `AsRef<str>`, including `&str`,
106    /// `String`, and [`SearchCriteria`](crate::types::SearchCriteria).
107    pub async fn search(
108        &self,
109        criteria: impl AsRef<str>,
110        timeout: Duration,
111    ) -> Result<SearchResult, Error> {
112        let criteria = criteria.as_ref();
113        self.search_impl(
114            criteria,
115            Command::Search {
116                criteria: criteria.to_owned(),
117            },
118            "SEARCH",
119            timeout,
120        )
121        .await
122    }
123
124    /// SEARCH with RETURN options (RFC 4731 Section 3.2).
125    ///
126    /// Returns the full [`EsearchResponse`] with MIN, MAX, COUNT, and ALL fields.
127    /// Requires the server to advertise the `ESEARCH` capability.
128    ///
129    /// RFC 4731 Section 3.2: an extended SEARCH with RETURN options causes the
130    /// server to return a single ESEARCH response instead of a SEARCH response.
131    ///
132    /// Accepts anything that implements `AsRef<str>`, including `&str`,
133    /// `String`, and [`SearchCriteria`](crate::types::SearchCriteria).
134    pub async fn search_esearch(
135        &self,
136        criteria: impl AsRef<str>,
137        return_opts: &[&str],
138        timeout: Duration,
139    ) -> Result<EsearchResponse, Error> {
140        let criteria = criteria.as_ref();
141        self.search_esearch_impl(
142            criteria,
143            Command::SearchReturn {
144                criteria: criteria.to_owned(),
145                return_opts: return_opts.iter().map(|s| (*s).to_owned()).collect(),
146            },
147            "SEARCH RETURN",
148            timeout,
149        )
150        .await
151    }
152
153    /// SEARCH RETURN (SAVE) by sequence number (RFC 5182 Section 2).
154    ///
155    /// Saves the search results server-side as `$` for use in subsequent commands.
156    /// Does not return UIDs — the results are stored on the server.
157    ///
158    /// Accepts anything that implements `AsRef<str>`, including `&str`,
159    /// `String`, and [`SearchCriteria`](crate::types::SearchCriteria).
160    pub async fn search_save(
161        &self,
162        criteria: impl AsRef<str>,
163        timeout: Duration,
164    ) -> Result<(), Error> {
165        let criteria = criteria.as_ref();
166        self.search_save_impl(
167            criteria,
168            Command::SearchSave {
169                criteria: criteria.to_owned(),
170            },
171            timeout,
172        )
173        .await
174    }
175
176    /// UID SEARCH RETURN (SAVE) (RFC 5182 Section 2).
177    ///
178    /// Saves the search results server-side as `$` for use in subsequent commands.
179    /// Does not return UIDs — the results are stored on the server.
180    ///
181    /// Accepts anything that implements `AsRef<str>`, including `&str`,
182    /// `String`, and [`SearchCriteria`](crate::types::SearchCriteria).
183    pub async fn uid_search_save(
184        &self,
185        criteria: impl AsRef<str>,
186        timeout: Duration,
187    ) -> Result<(), Error> {
188        let criteria = criteria.as_ref();
189        self.search_save_impl(
190            criteria,
191            Command::UidSearchSave {
192                criteria: criteria.to_owned(),
193            },
194            timeout,
195        )
196        .await
197    }
198
199    /// Shared implementation for SEARCH RETURN (SAVE) and UID SEARCH RETURN (SAVE)
200    /// (RFC 5182 Section 2).
201    pub(super) async fn search_save_impl(
202        &self,
203        criteria: &str,
204        cmd: Command,
205        timeout: Duration,
206    ) -> Result<(), Error> {
207        self.require_state(&[SessionState::Selected])?;
208        // RFC 6855 Section 6: SEARCH RETURN (SAVE) remains part of the SEARCH
209        // command family and must be rejected until UTF8=ACCEPT is enabled.
210        self.check_utf8_only_enforced()?;
211        self.validate_search_criteria_capabilities(criteria)?;
212        // RFC 5182 Section 2: SEARCHRES capability is required.
213        self.require_searchres()?;
214        tokio::time::timeout(
215            timeout,
216            self.submit_regular(cmd, dispatch::SearchSaveConsumer::new()),
217        )
218        .await
219        .map_err(|_| Error::Timeout)??
220    }
221
222    /// STORE by sequence number (RFC 3501 Section 6.4.6).
223    ///
224    /// Prefer [`uid_store`](Self::uid_store) for stable message references.
225    ///
226    /// `\Recent` and `\*` are silently filtered per RFC 3501 Section 2.3.2
227    /// (they are not valid in STORE flag lists).
228    pub async fn store(
229        &self,
230        sequence_set: &SequenceSet,
231        operation: StoreOperation,
232        flags: &[Flag],
233        unchanged_since: Option<u64>,
234        timeout: Duration,
235    ) -> Result<StoreResult, Error> {
236        // RFC 5182 Section 2: `$` references saved search results and requires SEARCHRES.
237        if sequence_set.as_str().contains('$') {
238            self.require_searchres()?;
239        }
240        // RFC 3501 Section 2.3.2 / Section 9: \Recent is server-only and \*
241        // is not valid in STORE flag lists. Filter them out before encoding,
242        // consistent with append() (RFC 3501 Section 6.3.11).
243        let filtered_flags = filter_store_flags(flags);
244        self.store_impl(
245            Command::Store {
246                sequence_set: sequence_set.clone(),
247                operation,
248                flags: filtered_flags,
249                unchanged_since,
250            },
251            unchanged_since,
252            timeout,
253        )
254        .await
255    }
256
257    /// COPY by sequence number (RFC 3501 Section 6.4.7, RFC 4315 Section 3).
258    ///
259    /// Prefer [`uid_copy`](Self::uid_copy) for stable message references.
260    ///
261    /// On success, returns the server's response code, which SHOULD be
262    /// `[COPYUID uid-validity source-uids dest-uids]` per RFC 4315 Section 3.
263    pub async fn copy(
264        &self,
265        sequence_set: &SequenceSet,
266        mailbox: &str,
267        timeout: Duration,
268    ) -> Result<CopyResult, Error> {
269        // RFC 5182 Section 2: `$` references saved search results and requires SEARCHRES.
270        if sequence_set.as_str().contains('$') {
271            self.require_searchres()?;
272        }
273        let mbox = MailboxName::new(mailbox)?;
274        self.copy_impl(
275            Command::Copy {
276                sequence_set: sequence_set.clone(),
277                mailbox: mbox,
278            },
279            timeout,
280        )
281        .await
282    }
283
284    /// MOVE by sequence number (RFC 6851 Section 3, RFC 6851 Section 4.3).
285    ///
286    /// Prefer [`uid_move_messages`](Self::uid_move_messages) for stable message references.
287    ///
288    /// Returns a [`MoveResult`] containing:
289    /// - `code`: the server's response code, typically `[COPYUID ...]` per
290    ///   RFC 6851 Section 4.3.
291    /// - `expunged`: the EXPUNGE or VANISHED responses sent before the tagged
292    ///   OK (RFC 6851 Section 3). When QRESYNC is enabled (RFC 7162
293    ///   Section 3.2.10), the server sends VANISHED instead of EXPUNGE.
294    pub async fn move_messages(
295        &self,
296        sequence_set: &SequenceSet,
297        mailbox: &str,
298        timeout: Duration,
299    ) -> Result<MoveResult, Error> {
300        self.require_state(&[SessionState::Selected])?;
301        self.check_utf8_only_enforced()?;
302        // RFC 5182 Section 2: `$` references saved search results and requires SEARCHRES.
303        if sequence_set.as_str().contains('$') {
304            self.require_searchres()?;
305        }
306        // MOVE is a base feature in IMAP4rev2 (RFC 9051 Appendix E item 2).
307        {
308            let snap = self.state_rx.borrow();
309            if !snap.capabilities.contains(&Capability::Move)
310                && !super::auth::is_rev2_from_snapshot(&snap)
311            {
312                return Err(Error::MissingCapability("MOVE".into()));
313            }
314        }
315        let mbox = MailboxName::new(mailbox)?;
316        self.move_native_impl(
317            Command::Move {
318                sequence_set: sequence_set.clone(),
319                mailbox: mbox,
320            },
321            timeout,
322        )
323        .await
324    }
325}