/// Step 5: In-memory message store. /// /// Maps IMAP sequence numbers (1-based, contiguous) ↔ ProtonMail message IDs. /// Messages are stored oldest-first so that seq 1 = oldest, seq N = newest. /// The API returns messages newest-first, so `load_all` reverses the slice. /// /// After EXPUNGE the Vec automatically renumbers: the element that was at /// index i is now at some new index j, and its seq number is j + 1. use std::collections::HashSet; use crate::api::MessageMeta; pub struct MessageStore { /// Oldest-first. `messages[i]` has IMAP sequence number `i + 1`. messages: Vec, /// Proton IDs of messages marked `\Deleted`, waiting for EXPUNGE. deleted_pending: HashSet, } impl MessageStore { pub fn new() -> Self { Self { messages: Vec::new(), deleted_pending: HashSet::new(), } } /// Replace the store contents with a fresh API response (newest-first). /// Reverses to oldest-first and clears any pending deletions. pub fn load_all(&mut self, mut messages: Vec) { messages.reverse(); // API is newest-first; store oldest-first self.messages = messages; self.deleted_pending.clear(); } /// Number of messages currently in the store (the IMAP EXISTS count). pub fn count(&self) -> u32 { self.messages.len() as u32 } /// Look up the ProtonMail message ID for a given IMAP sequence number. pub fn proton_id(&self, seq: u32) -> Option<&str> { self.messages .get(seq.wrapping_sub(1) as usize) .map(|m| m.id.as_str()) } /// Look up message metadata for a given IMAP sequence number. pub fn get_meta(&self, seq: u32) -> Option<&MessageMeta> { self.messages.get(seq.wrapping_sub(1) as usize) } /// Return all (seq, meta) pairs in the inclusive range [from, to]. /// Clamps to the actual message count. pub fn get_range(&self, from: u32, to: u32) -> Vec<(u32, &MessageMeta)> { let max = self.count(); let from = from.max(1); let to = to.min(max); if from > to { return vec![]; } (from..=to) .filter_map(|seq| self.get_meta(seq).map(|m| (seq, m))) .collect() } /// Mark a message for deletion (STORE … +FLAGS (\Deleted)). pub fn mark_deleted(&mut self, seq: u32) { if let Some(m) = self.get_meta(seq) { self.deleted_pending.insert(m.id.clone()); } } /// Remove all pending-deleted messages. /// Returns `(proton_ids, seqs)` where `seqs` are the pre-expunge IMAP /// sequence numbers in **descending** order (correct for IMAP EXPUNGE /// responses — highest seq first so client renumbering stays consistent). pub fn expunge(&mut self) -> (Vec, Vec) { let mut removed_ids = Vec::new(); let mut removed_seqs = Vec::new(); for (i, m) in self.messages.iter().enumerate() { if self.deleted_pending.contains(&m.id) { removed_ids.push(m.id.clone()); removed_seqs.push(i as u32 + 1); } } self.messages .retain(|m| !self.deleted_pending.contains(&m.id)); self.deleted_pending.clear(); removed_seqs.sort_unstable_by(|a, b| b.cmp(a)); // descending (removed_ids, removed_seqs) } /// Return the seq numbers of messages whose subject, sender name, or /// sender address contains `query` (case-insensitive substring match). pub fn search_subject_or_from(&self, query: &str) -> Vec { let q = query.to_lowercase(); self.messages .iter() .enumerate() .filter(|(_, m)| { m.subject.to_lowercase().contains(&q) || m.sender.address.to_lowercase().contains(&q) || m.sender.name.to_lowercase().contains(&q) }) .map(|(i, _)| i as u32 + 1) .collect() } }