Skip to main content

io_email/envelope/imap/
diff.rs

1//! Helpers for incremental envelope sync (CONDSTORE / QRESYNC) used
2//! by [`crate::client::EmailClientStd::diff_envelopes`].
3
4use core::num::NonZeroU32;
5
6use alloc::{
7    format,
8    string::{String, ToString},
9    vec,
10    vec::Vec,
11};
12
13use io_imap::types::{
14    fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
15    flag::FlagFetch,
16};
17
18use crate::{
19    envelope::{
20        imap::list::envelope_from,
21        types::{Envelope, FlagUpdate},
22    },
23    flag::types::Flag,
24};
25
26/// Wire size of an [`ImapState`]: uid_validity (u32) + highest_mod_seq
27/// (u64) + highest_uid (u32), little-endian.
28const STATE_BYTES: usize = 4 + 8 + 4;
29
30/// IMAP sync checkpoint: uid_validity + highest_mod_seq feed SELECT
31/// (QRESYNC ...); highest_uid scopes the follow-up UID FETCH
32/// high+1:*.
33#[derive(Clone, Copy, Debug, Default)]
34pub struct ImapState {
35    pub uid_validity: u32,
36    pub highest_mod_seq: u64,
37    pub highest_uid: u32,
38}
39
40impl ImapState {
41    /// Decodes a byte blob produced by [`Self::encode`]; `None` on
42    /// length mismatch (treat as no usable state).
43    pub fn decode(bytes: &[u8]) -> Option<Self> {
44        if bytes.len() != STATE_BYTES {
45            return None;
46        }
47
48        let uid_validity = u32::from_le_bytes(bytes[0..4].try_into().ok()?);
49        let highest_mod_seq = u64::from_le_bytes(bytes[4..12].try_into().ok()?);
50        let highest_uid = u32::from_le_bytes(bytes[12..16].try_into().ok()?);
51
52        Some(Self {
53            uid_validity,
54            highest_mod_seq,
55            highest_uid,
56        })
57    }
58
59    /// Encodes the checkpoint into a 16-byte little-endian blob.
60    pub fn encode(&self) -> Vec<u8> {
61        let mut out = Vec::with_capacity(STATE_BYTES);
62        out.extend_from_slice(&self.uid_validity.to_le_bytes());
63        out.extend_from_slice(&self.highest_mod_seq.to_le_bytes());
64        out.extend_from_slice(&self.highest_uid.to_le_bytes());
65        out
66    }
67}
68
69/// FETCH items for the follow-up UID FETCH high+1:*; same set as
70/// the envelope-list coroutine but without BodyStructure.
71pub fn new_message_item_names() -> MacroOrMessageDataItemNames<'static> {
72    MacroOrMessageDataItemNames::MessageDataItemNames(vec![
73        MessageDataItemName::Uid,
74        MessageDataItemName::Flags,
75        MessageDataItemName::Envelope,
76        MessageDataItemName::Rfc822Size,
77    ])
78}
79
80/// UID sequence-set high+1:*; `None` on overflow.
81pub fn new_message_window(high: u32) -> Option<String> {
82    let start = high.checked_add(1)?;
83    Some(format!("{start}:*"))
84}
85
86/// Translates one QRESYNC implicit `* FETCH` payload into a
87/// [`FlagUpdate`]; `None` when neither UID nor FLAGS were surfaced.
88pub fn flag_update_from_items(items: &[MessageDataItem<'static>]) -> Option<FlagUpdate> {
89    let mut uid: Option<NonZeroU32> = None;
90    let mut flags: Option<alloc::collections::BTreeSet<Flag>> = None;
91
92    for item in items {
93        match item {
94            MessageDataItem::Uid(u) => uid = Some(*u),
95            MessageDataItem::Flags(fs) => {
96                flags = Some(
97                    fs.iter()
98                        .filter_map(|f| match f {
99                            FlagFetch::Flag(flag) => Some(Flag::from_raw(flag.to_string())),
100                            _ => None,
101                        })
102                        .collect(),
103                );
104            }
105            _ => {}
106        }
107    }
108
109    Some(FlagUpdate {
110        id: uid?.get().to_string(),
111        flags: flags?,
112    })
113}
114
115/// Builds an [`Envelope`] from a FETCH item list; thin wrapper over
116/// the envelope-list converter.
117pub fn envelope_from_items(items: Vec<MessageDataItem<'static>>) -> Envelope {
118    envelope_from(0, items)
119}