diff --git a/core/src/store.rs b/core/src/store.rs index 1deada1..48ca556 100644 --- a/core/src/store.rs +++ b/core/src/store.rs @@ -571,6 +571,23 @@ impl Store { .is_some()) } + /// Removes one message from the local store — the reader's delete, which + /// the server-side DELETE (sec 6.1) knows nothing about. The id is marked + /// seen (sec 10), so a copy still sitting on the server is fetched again + /// without being re-stored: deleting locally must not resurrect the + /// message on the next fetch. + pub fn delete_local(&self, folder: &str, id: &[u8; ID_LEN]) -> Result<(), Error> { + let table = match folder { + "sent" => "sent", + _ => "inbox", + }; + self.db() + .execute(&format!("DELETE FROM {table} WHERE id = ?1"), params![id])?; + self.db() + .execute("INSERT OR IGNORE INTO seen (id, at) VALUES (?1, ?2)", params![id, now()])?; + Ok(()) + } + /// One folder, ordered by arrival or sending time. `folder` is "inbox", /// "requests", "sent" or "all". pub fn mail(&self, folder: &str) -> Result, Error> { @@ -887,6 +904,24 @@ mod tests { assert!(store.history("b@example.org").unwrap().is_empty()); } + #[test] + fn local_delete_marks_seen_so_a_refetch_does_not_restore() { + let store = temp_store("delete-local"); + let id = [3u8; 32]; + store.store_inbox(&id, b"envelope", 100, false, true).unwrap(); + store.delete_local("inbox", &id).unwrap(); + assert!(store.mail("all").unwrap().is_empty()); + // Sec 10: the id is seen, so the fetch pipeline will not re-store + // the still-kept server copy. (The dedupe is the pipeline's seen + // check, not store_inbox's — a direct insert is a caller override.) + assert!(store.seen(&id).unwrap()); + // Sent copies delete locally too, with the same protection. + let sent_id = [4u8; 32]; + store.store_sent(&sent_id, "bob@example.org", b"sent", 50).unwrap(); + store.delete_local("sent", &sent_id).unwrap(); + assert!(store.mail("sent").unwrap().is_empty()); + } + #[test] fn in_memory_store_round_trips() { let store = Store::open_in_memory().expect("memory store opens");