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}