Skip to main content

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;