daaki_imap/types/fetch.rs
1//! FETCH response and request types, APPEND helpers, and BINARY extension types.
2//!
3//! FETCH responses are defined in RFC 3501 Section 7.4.2 / RFC 9051 Section 7.5.2.
4//! FETCH command attributes are defined in RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5.
5//! MULTIAPPEND is defined in RFC 3502.
6//! BINARY extension is defined in RFC 3516.
7
8use super::{BodyStructure, Envelope, Flag};
9
10/// A parsed FETCH response for a single message
11/// (RFC 3501 Section 7.4.2 / RFC 9051 Section 7.5.2).
12#[non_exhaustive]
13#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub struct FetchResponse {
16 /// Message sequence number (RFC 3501 Section 7.4.2 / RFC 9051 Section 7.5.2).
17 pub seq: u32,
18 /// `UID` value (RFC 3501 Section 2.3.1.1).
19 pub uid: Option<u32>,
20 /// `FLAGS` value (RFC 3501 Section 2.3.2).
21 pub flags: Option<Vec<Flag>>,
22 /// Parsed `ENVELOPE` (RFC 3501 Section 7.4.2).
23 pub envelope: Option<Envelope>,
24 /// Parsed `BODYSTRUCTURE` (RFC 3501 Section 7.4.2).
25 pub body_structure: Option<BodyStructure>,
26 /// `RFC822.SIZE` in bytes (RFC 3501 Section 7.4.2 / RFC 9051 Section 7.5.2).
27 ///
28 /// RFC 9051 widens this from `number` to `number64` to support messages > 4 GiB.
29 pub rfc822_size: Option<u64>,
30 /// `INTERNALDATE` timestamp string (RFC 3501 Section 7.4.2).
31 ///
32 /// The date-time string in IMAP format, e.g. `"17-Jul-1996 02:44:25 -0700"`.
33 pub internal_date: Option<String>,
34 /// Fetched body sections per `BODY[section]<partial>` (RFC 3501 Section 6.4.5).
35 pub body_sections: Vec<BodySection>,
36 /// `MODSEQ` value (RFC 7162 CONDSTORE Section 3.1.3).
37 pub mod_seq: Option<u64>,
38 /// `SAVEDATE` timestamp string (RFC 8514 Section 3).
39 ///
40 /// The date-time when the message was saved to the mailbox, or `None` if
41 /// the server returned NIL (message predates SAVEDATE tracking).
42 pub save_date: Option<String>,
43 /// `BINARY[section]` data items (RFC 3516 Section 4.2).
44 pub binary_sections: Vec<BinarySection>,
45 /// `BINARY.SIZE[section]` values (RFC 3516 Section 4.3 / RFC 9051 Section 7.5.2).
46 ///
47 /// Each entry is `(section_parts, size_in_bytes)`.
48 /// RFC 9051 Section 9 defines the size as `number` (u32), but stored as
49 /// `u64` for Postel's-law leniency with servers that send larger values.
50 pub binary_sizes: Vec<(Vec<u32>, u64)>,
51 /// `PREVIEW` text (RFC 8970 Section 3).
52 ///
53 /// A short plaintext snippet of the message, or `None` if the server
54 /// returned NIL (e.g., for LAZY requests where the preview is not yet computed).
55 pub preview: Option<String>,
56 /// `EMAILID` value (RFC 8474 Section 4).
57 ///
58 /// A server-assigned unique identifier for this particular instance of a message.
59 /// The value is an `objectid` string (1-255 alphanumeric/dash/underscore characters).
60 pub email_id: Option<String>,
61 /// `THREADID` value (RFC 8474 Section 4).
62 ///
63 /// A server-assigned identifier grouping related messages into threads,
64 /// or `None` if the server returned NIL (no thread association).
65 pub thread_id: Option<String>,
66}
67
68/// A single fetched BINARY section (RFC 3516 Section 4.2).
69///
70/// Returned as `BINARY[section]<origin>` in FETCH responses.
71/// The content has been decoded from its Content-Transfer-Encoding
72/// (RFC 3516 Section 4.2).
73#[non_exhaustive]
74#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76pub struct BinarySection {
77 /// The numeric section path (e.g. `[1, 2, 3]` for `BINARY[1.2.3]`).
78 pub section: Vec<u32>,
79 /// Byte offset if this was a partial fetch (`<origin>` per RFC 3516 Section 4.2).
80 ///
81 /// RFC 9051 Section 7.5.2 widens this from `number` (u32) to `number64` (u64)
82 /// to support messages > 4 GiB.
83 pub origin: Option<u64>,
84 /// The decoded binary data, or `None` if the server returned NIL.
85 pub data: Option<Vec<u8>>,
86}
87
88/// A single fetched body section (RFC 3501 Section 6.4.5).
89#[non_exhaustive]
90#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub struct BodySection {
93 /// The section specification (e.g. `"HEADER"`, `"1.2"`, `"TEXT"`).
94 pub section: String,
95 /// Byte offset if this was a partial fetch (`<origin.count>`).
96 ///
97 /// RFC 9051 Section 7.5.2 widens this from `number` (u32) to `number64` (u64)
98 /// to support messages > 4 GiB.
99 pub origin: Option<u64>,
100 /// The raw body data, or `None` if the server returned NIL.
101 pub data: Option<Vec<u8>>,
102}
103
104/// FETCH item attributes the client can request
105/// (RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5).
106#[non_exhaustive]
107#[derive(Debug, Clone, PartialEq, Eq, Hash)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109pub enum FetchAttr {
110 /// UID (RFC 3501 Section 2.3.1.1).
111 Uid,
112 /// FLAGS (RFC 3501 Section 2.3.2).
113 Flags,
114 /// ENVELOPE (RFC 3501 Section 7.4.2).
115 Envelope,
116 /// BODYSTRUCTURE (RFC 3501 Section 7.4.2).
117 BodyStructure,
118 /// RFC822.SIZE (RFC 3501 Section 7.4.2).
119 Rfc822Size,
120 /// INTERNALDATE (RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5).
121 InternalDate,
122 /// RFC822 — functionally equivalent to `BODY[]` (RFC 3501 Section 6.4.5).
123 Rfc822,
124 /// RFC822.HEADER — functionally equivalent to `BODY.PEEK[HEADER]` (RFC 3501 Section 6.4.5).
125 Rfc822Header,
126 /// RFC822.TEXT — functionally equivalent to `BODY[TEXT]` (RFC 3501 Section 6.4.5).
127 Rfc822Text,
128 /// `BODY.PEEK[section]<partial>` or `BODY[section]<partial>`
129 /// (RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5).
130 ///
131 /// RFC 9051 widens partial ranges from `number` to `number64` / `nz-number64`.
132 BodySection {
133 peek: bool,
134 section: Option<String>,
135 partial: Option<(u64, u64)>,
136 },
137 /// MODSEQ (RFC 7162 CONDSTORE Section 3.1.3).
138 ModSeq,
139 /// SAVEDATE (RFC 8514 Section 3).
140 ///
141 /// Returns the date-time the message was saved to the mailbox.
142 /// Value is a quoted date-time string (same format as INTERNALDATE) or NIL.
143 SaveDate,
144 /// `BINARY[section]<partial>` or `BINARY.PEEK[section]<partial>`
145 /// (RFC 3516 Section 4.5.1).
146 ///
147 /// Fetches the content of a MIME part, decoded from its
148 /// Content-Transfer-Encoding. The section is a dot-separated list of
149 /// part numbers (e.g. `[1, 2]` for section `1.2`).
150 Binary {
151 /// Whether to use `BINARY.PEEK` (no `\Seen` flag set) or `BINARY`.
152 peek: bool,
153 /// Numeric section path (e.g. `[1, 2, 3]`).
154 section: Vec<u32>,
155 /// Optional partial range `(offset, count)`.
156 ///
157 /// RFC 9051 widens partial ranges from `number` to `number64` / `nz-number64`.
158 partial: Option<(u64, u64)>,
159 },
160 /// `BINARY.SIZE[section]` (RFC 3516 Section 4.5.2).
161 ///
162 /// Returns the decoded size in bytes of a MIME part.
163 BinarySize {
164 /// Numeric section path (e.g. `[1, 2, 3]`).
165 section: Vec<u32>,
166 },
167 /// `PREVIEW` (RFC 8970 Section 3).
168 ///
169 /// Returns a short plaintext snippet of the message. The server generates
170 /// the preview text from the message body.
171 Preview,
172 /// `PREVIEW (LAZY)` (RFC 8970 Section 3).
173 ///
174 /// Like `Preview`, but allows the server to return NIL if the preview
175 /// is not yet computed, avoiding delays.
176 PreviewLazy,
177 /// `EMAILID` (RFC 8474 Section 4).
178 ///
179 /// Returns the server-assigned unique identifier for this message instance.
180 EmailId,
181 /// `THREADID` (RFC 8474 Section 4).
182 ///
183 /// Returns the server-assigned thread identifier, or NIL if no thread association.
184 ThreadId,
185}
186
187impl FetchAttr {
188 /// Serialize this attribute to its IMAP wire representation
189 /// (RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5).
190 pub fn to_imap_string(&self) -> String {
191 use std::fmt::Write;
192 match self {
193 Self::Uid => "UID".into(),
194 Self::Flags => "FLAGS".into(),
195 Self::Envelope => "ENVELOPE".into(),
196 Self::BodyStructure => "BODYSTRUCTURE".into(),
197 Self::Rfc822Size => "RFC822.SIZE".into(),
198 Self::InternalDate => "INTERNALDATE".into(),
199 Self::Rfc822 => "RFC822".into(),
200 Self::Rfc822Header => "RFC822.HEADER".into(),
201 Self::Rfc822Text => "RFC822.TEXT".into(),
202 Self::BodySection {
203 peek,
204 section,
205 partial,
206 } => {
207 // RFC 3501 Section 6.4.5: BODY[<section>]<<partial>>
208 // or BODY.PEEK[<section>]<<partial>>.
209 let mut s = if *peek {
210 "BODY.PEEK[".to_owned()
211 } else {
212 "BODY[".to_owned()
213 };
214 if let Some(sec) = section {
215 s.push_str(sec);
216 }
217 s.push(']');
218 if let Some((offset, count)) = partial {
219 // RFC 3501 Section 6.4.5: partial range <offset.count>.
220 let _ = write!(s, "<{offset}.{count}>");
221 }
222 s
223 }
224 Self::ModSeq => "MODSEQ".into(),
225 Self::SaveDate => "SAVEDATE".into(),
226 Self::Binary {
227 peek,
228 section,
229 partial,
230 } => {
231 // RFC 3516 Section 4.5.1: BINARY[section]<partial>
232 // or BINARY.PEEK[section]<partial>.
233 let mut s = if *peek {
234 "BINARY.PEEK[".to_owned()
235 } else {
236 "BINARY[".to_owned()
237 };
238 let sec_str: Vec<String> = section
239 .iter()
240 .map(std::string::ToString::to_string)
241 .collect();
242 s.push_str(&sec_str.join("."));
243 s.push(']');
244 if let Some((offset, count)) = partial {
245 // RFC 3516 Section 4.5.1: partial range <offset.count>.
246 let _ = write!(s, "<{offset}.{count}>");
247 }
248 s
249 }
250 Self::BinarySize { section } => {
251 // RFC 3516 Section 4.5.2: BINARY.SIZE[section].
252 let sec_str: Vec<String> = section
253 .iter()
254 .map(std::string::ToString::to_string)
255 .collect();
256 format!("BINARY.SIZE[{}]", sec_str.join("."))
257 }
258 Self::Preview => "PREVIEW".into(),
259 Self::PreviewLazy => "PREVIEW (LAZY)".into(),
260 Self::EmailId => "EMAILID".into(),
261 Self::ThreadId => "THREADID".into(),
262 }
263 }
264}
265
266/// Serialize a list of fetch attributes to the parenthesized IMAP wire format
267/// (RFC 3501 Section 6.4.5 / RFC 9051 Section 6.4.5).
268///
269/// A single attribute is serialized without parentheses; multiple attributes
270/// are joined with spaces inside parentheses.
271pub(crate) fn format_fetch_attrs(attrs: &[FetchAttr]) -> String {
272 if attrs.len() == 1 {
273 attrs[0].to_imap_string()
274 } else {
275 let items: Vec<String> = attrs.iter().map(FetchAttr::to_imap_string).collect();
276 format!("({})", items.join(" "))
277 }
278}
279
280/// How flags should be modified in a STORE command
281/// (RFC 3501 Section 6.4.6 / RFC 9051 Section 6.4.6).
282///
283/// RFC 3501 Section 6.4.6 defines:
284/// `store-att-flags = (["+" / "-"] "FLAGS" [".SILENT"]) SP (flag-list / NIL)`
285#[non_exhaustive]
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
287#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
288pub enum StoreOperation {
289 /// `+FLAGS` — add flags.
290 Add,
291 /// `-FLAGS` — remove flags.
292 Remove,
293 /// `FLAGS` — replace all flags.
294 Replace,
295 /// `+FLAGS.SILENT` — add flags, suppress implicit FETCH response
296 /// (RFC 3501 Section 6.4.6).
297 AddSilent,
298 /// `-FLAGS.SILENT` — remove flags, suppress implicit FETCH response
299 /// (RFC 3501 Section 6.4.6).
300 RemoveSilent,
301 /// `FLAGS.SILENT` — replace all flags, suppress implicit FETCH response
302 /// (RFC 3501 Section 6.4.6).
303 ReplaceSilent,
304}
305
306/// Result of a STORE or UID STORE command (RFC 3501 Section 6.4.6, RFC 7162 Section 3.1.3).
307///
308/// When UNCHANGEDSINCE is used (CONDSTORE), the server may return a
309/// `[MODIFIED sequence-set]` response code indicating which messages failed
310/// the precondition check (RFC 7162 Section 3.1.3). `STORE` uses message
311/// sequence numbers, while `UID STORE` uses UIDs. This struct preserves that
312/// information so callers can detect partial failures.
313#[non_exhaustive]
314#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
315#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
316pub struct StoreResult {
317 /// Implicit FETCH responses with updated flags (RFC 3501 Section 6.4.6).
318 ///
319 /// Empty when `.SILENT` operations are used.
320 pub fetches: Vec<FetchResponse>,
321 /// Response code from the tagged OK, if any.
322 ///
323 /// When UNCHANGEDSINCE is used, this will be `Some(ResponseCode::Modified(...))`
324 /// for messages that failed the precondition (RFC 7162 Section 3.1.3).
325 /// `None` when no response code was returned.
326 pub code: Option<super::response::ResponseCode>,
327}
328
329/// A message to be appended via MULTIAPPEND (RFC 3502).
330///
331/// Each message carries its own flags, optional internal date, and raw message data.
332/// Multiple `AppendMessage` values are sent in a single APPEND command per
333/// RFC 3502 Section 3.
334#[non_exhaustive]
335#[derive(Debug, Clone, PartialEq, Eq, Hash)]
336#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
337pub struct AppendMessage {
338 /// Flags to set on the appended message (RFC 3501 Section 6.3.11).
339 pub flags: Vec<Flag>,
340 /// Optional INTERNALDATE in IMAP date-time format (RFC 3501 Section 6.3.11).
341 pub date: Option<String>,
342 /// Raw RFC 5322 message data.
343 pub data: Vec<u8>,
344}
345
346impl AppendMessage {
347 /// Create an append message with only the raw data (RFC 3501 Section 6.3.11).
348 ///
349 /// Flags default to empty and date defaults to `None`.
350 pub fn new(data: impl Into<Vec<u8>>) -> Self {
351 Self {
352 flags: Vec::new(),
353 date: None,
354 data: data.into(),
355 }
356 }
357}
358
359#[cfg(test)]
360#[path = "fetch_tests.rs"]
361mod tests;