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