From 6d277d588a439485d0a40443dbedbbdd34a0f958 Mon Sep 17 00:00:00 2001 From: FliegendeWurst <2012gdwu+github@posteo.de> Date: Sun, 13 Feb 2022 15:22:16 +0100 Subject: [PATCH] sync: refactor to action architecture this makes for an easy --dry-run implementation --- inboxid-sync/src/lib.rs | 219 +++++++++++++++++++++++++ inboxid-sync/src/main.rs | 338 +++++++++++++++++---------------------- 2 files changed, 369 insertions(+), 188 deletions(-) create mode 100644 inboxid-sync/src/lib.rs diff --git a/inboxid-sync/src/lib.rs b/inboxid-sync/src/lib.rs new file mode 100644 index 0000000..d1ecbfc --- /dev/null +++ b/inboxid-sync/src/lib.rs @@ -0,0 +1,219 @@ +use std::{collections::HashMap, borrow::Cow, fmt::Display}; + +use anyhow::Context; +use imap::types::{Flag, NameAttribute}; +use itertools::Itertools; +use maildir::Maildir; + +use inboxid_lib::*; +use mailparse::parse_header; +use rusqlite::{Row, params, types::FromSql}; + +pub static TRASH: NameAttribute = NameAttribute::Custom(Cow::Borrowed("\\Trash")); + +pub enum SyncAction { + TrashRemote(String, MaildirID), + TrashLocal(String, MaildirID), + DeleteRemote(String, MaildirID), + DeleteLocal(String, MaildirID), + UpdateFlags(String, Vec<(MaildirID, Vec>, String)>), + Hardlink(String, Vec<(MaildirID, String, Vec>)>), + Fetch(String, Vec), + RemoveStale(HashMap>) +} + +impl SyncAction { + pub fn mailbox(&self) -> Option<&str> { + match self { + TrashRemote(mailbox, _) => Some(mailbox), + TrashLocal(mailbox, _) => Some(mailbox), + DeleteRemote(mailbox, _) => Some(mailbox), + DeleteLocal(mailbox, _) => Some(mailbox), + UpdateFlags(mailbox, _) => Some(mailbox), + Hardlink(mailbox, _) => Some(mailbox), + Fetch(mailbox, _) => Some(mailbox), + RemoveStale(_) => None, + } + } +} + +use SyncAction::*; + +impl Display for SyncAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TrashRemote(mailbox, id) => write!(f, "thrash remotely: {}/{}\n", mailbox, id)?, + TrashLocal(mailbox, id) => write!(f, "thrash locally: {}/{}\n", mailbox, id)?, + DeleteRemote(mailbox, id) => write!(f, "delete remotely: {}/{}\n", mailbox, id)?, + DeleteLocal(mailbox, id) => write!(f, "delete locally: {}/{}\n", mailbox, id)?, + UpdateFlags(mailbox, _) => write!(f, "updating flags of mail in {}\n", mailbox)?, + Hardlink(mailbox, id) => write!(f, "hardlink from local: {}/{:?}\n", mailbox, id)?, + Fetch(mailbox, id) => write!(f, "fetch: {}/{:?}", mailbox, id)?, + RemoveStale(map) => write!(f, "remove stale mail: {:?}", map)?, + } + Ok(()) + } +} + +pub fn compute_sync_actions( + host: &str, + user: &str, + password: &str, + port: u16, + mailboxes: &[String] +) -> Result<(Vec, HashMap>)>>)> { + let mut actions = Vec::new(); + + let mut db = get_db()?; + let mut imap_session = connect(host, port, user, password)?; + println!("getting capabilities.."); + let caps = imap_session.capabilities()?; + println!("capabilities: {}", caps.iter().map(|x| format!("{:?}", x)).join(" ")); + + let mut names = Vec::new(); + let list = imap_session.list(None, Some("*"))?; + for x in list.iter() { + println!("{:?}", x); + names.push(x); + } + + let mut remote = HashMap::new(); + + for &name in &names { + let mailbox = name.name(); + // if the user specified some mailboxes, only process those + if !mailboxes.is_empty() && !mailboxes.iter().any(|x| x == mailbox) { + continue; + } + println!("indexing {}", mailbox); + let resp = imap_session.examine(mailbox)?; + let uid_validity = resp.uid_validity.unwrap(); + + let mut mails = HashMap::new(); + let messages = imap_session.uid_fetch("1:*", "(FLAGS BODY[HEADER.FIELDS (MESSAGE-ID)])")?; + for m in messages.iter() { + let id = MaildirID::new(uid_validity, m.uid.unwrap()); + let flags = m.flags(); + if flags.contains(&Flag::Deleted) { + continue; + } + let header = m.header().unwrap(); + let mut message_id = parse_header(header).map(|x| x.0.get_value()).unwrap_or_default(); + if message_id.is_empty() { + message_id = fallback_mid(mailbox, id); + } + let flags = flags.iter().map(|x| remove_cow(x)).collect_vec(); + mails.insert(message_id, (id.uid_validity, id.uid, id, flags)); + } + remote.insert(mailbox.to_string(), mails); + } + + // start a transaction to fully simulate fetching behaviour (drop changes afterwards) + let tx = db.transaction()?; + let mut have_mail = tx.prepare("SELECT mailbox, uid, flags FROM mail WHERE message_id = ?")?; + let mut delete_mail = tx.prepare("DELETE FROM mail WHERE mailbox = ? AND uid = ?")?; + let mut all_mail = tx.prepare("SELECT uid, message_id, flags FROM mail WHERE mailbox = ?")?; + let mut save_mail = tx.prepare("INSERT INTO mail VALUES (?,?,?,?)")?; + let mut maildirs: HashMap = names.iter().map(|&x| (x.name().to_owned(), get_maildir(x.name()).unwrap())).collect(); + let mut printed_trash_warning = false; + let trash_dir = names.iter().filter(|x| x.attributes().iter().any(|x| *x == TRASH)).map(|x| x.name()).next(); + let mut to_remove: HashMap = HashMap::new(); + for &name in &names { + let mailbox = name.name(); + // if the user specified some mailboxes, only process those + if !mailboxes.is_empty() && !mailboxes.iter().any(|x| x == mailbox) { + continue; + } + let is_trash = name.attributes().iter().any(|x| *x == TRASH); + let remote_mails = remote.get_mut(mailbox).unwrap(); + println!("selecting {}", mailbox); + imap_session.select(mailbox).context("select failed")?; + let all_mails = all_mail.query_map(params![mailbox], map3rows::)?; + for x in all_mails { + let (uid, mid, flags) = x?; + let uid: MaildirID = uid.into(); + if flags.contains(TRASHED) && !is_trash { + if let Some(_) = trash_dir { + println!("trashing: {}/{}", mailbox, uid); + if remote_mails.contains_key(&mid) { + actions.push(TrashRemote(mailbox.to_owned(), uid)); + } else { + actions.push(TrashLocal(mailbox.to_owned(), uid)); + } + delete_mail.execute(params![mailbox, uid])?; + } else if !printed_trash_warning { + println!("Warning: unable to trash mail, no trash folder found!"); + printed_trash_warning = true; + } + } else if flags.contains(DELETE) { + println!("deleting: {}/{}", mailbox, uid); + if remote_mails.contains_key(&mid) { + actions.push(DeleteRemote(mailbox.to_owned(), uid)); + } else { + actions.push(DeleteLocal(mailbox.to_owned(), uid)); + } + delete_mail.execute(params![mailbox, uid])?; + } + } + + let mut to_flag = Vec::new(); + let mut to_fetch = Vec::new(); + let mut to_hardlink = Vec::new(); + for (message_id, entry) in remote_mails.iter_mut() { + let (uid1, uid2, full_uid, remote_flags) = entry; + let local = have_mail.query_map(params![message_id], map3rows::)?.map(|x| x.unwrap()).collect_vec(); + + if let Some((_, full_uid, flags)) = local.iter().filter(|x| x.0 == mailbox && x.1 == *full_uid).next() { + to_flag.push((*full_uid, remote_flags.clone(), flags.clone())); + continue; + } + if !local.is_empty() { + let (_, _, flags) = &local[0]; + let new_uid = MaildirID::new(*uid1, *uid2); + to_hardlink.push((new_uid, message_id.clone(), remote_flags.clone())); + save_mail.execute(params![mailbox, new_uid.to_i64(), message_id, flags])?; + } else if !is_trash { // do not fetch trashed mail + println!("fetching {:?} {:?} as it is not in {:?}", uid2, message_id, local); + let new_uid = MaildirID::new(*uid1, *uid2); + to_fetch.push(new_uid); + } + } + if !to_flag.is_empty() { + actions.push(UpdateFlags(mailbox.to_string(), to_flag)); + } + if !to_hardlink.is_empty() { + actions.push(Hardlink(mailbox.to_string(), to_hardlink)); + } + if !to_fetch.is_empty() { + actions.push(Fetch(mailbox.to_string(), to_fetch)); + } + + let mails = all_mail.query_map(params![mailbox], |row| + Ok((load_i64(row.get::<_, i64>(0)?), row.get::<_, String>(1)?)))? + .map(|x| x.unwrap()).collect_vec(); + let mut removed = Vec::new(); + for (uid, message_id) in mails { + let uid1 = (uid >> 32) as u32; + let uid2 = ((uid << 32) >> 32) as u32; + if !remote_mails.contains_key(&message_id) && !message_id.ends_with("@no-message-id>") { + removed.push((uid1, uid2, uid)); + } + } + if !removed.is_empty() { + to_remove.insert(mailbox.to_string(), removed); + } + } + actions.push(RemoveStale(to_remove)); + + // be nice to the server and log out + imap_session.logout()?; + + Ok((actions, remote)) +} + +pub fn map3rows(row: &Row) -> rusqlite::Result<(A, B, C)> { + let a = row.get::<_, A>(0)?; + let b = row.get::<_, B>(1)?; + let c = row.get::<_, C>(2)?; + Ok((a, b, c)) +} diff --git a/inboxid-sync/src/main.rs b/inboxid-sync/src/main.rs index fd92126..072c917 100644 --- a/inboxid-sync/src/main.rs +++ b/inboxid-sync/src/main.rs @@ -1,25 +1,27 @@ -use std::{borrow::Cow, collections::HashMap, env}; +use std::{env, collections::HashMap}; use anyhow::Context; -use imap::types::{Flag, NameAttribute}; +use imap::types::Flag; use itertools::Itertools; -use maildir::Maildir; use inboxid_lib::*; -use mailparse::{parse_header, parse_headers}; -use rusqlite::{Row, params, types::FromSql}; - -const TRASH: NameAttribute = NameAttribute::Custom(Cow::Borrowed("\\Trash")); +use inboxid_sync::*; +use inboxid_sync::SyncAction::*; +use maildir::Maildir; +use mailparse::parse_headers; +use rusqlite::params; fn main() -> Result<()> { let host = env::var("MAILHOST").expect("missing envvar MAILHOST"); let user = env::var("MAILUSER").expect("missing envvar MAILUSER"); let password = env::var("MAILPASSWORD").expect("missing envvar MAILPASSWORD"); let port = 993; - let args = env::args().skip(1).collect_vec(); - let args = args.iter().map(|x| &**x).collect_vec(); + let mut args = env::args().skip(1).peekable(); + let dry_run = args.peek().map(|x| x == "--dry-run").unwrap_or(false); - sync(&host, &user, &password, port, &args) + let args = args.collect_vec(); + + sync(&host, &user, &password, port, &args, dry_run) } fn sync( @@ -27,8 +29,17 @@ fn sync( user: &str, password: &str, port: u16, - mailboxes: &[&str] + mailboxes: &[String], + dry_run: bool ) -> Result<()> { + let (actions, remote) = compute_sync_actions(host, user, password, port, mailboxes)?; + if dry_run { + for action in actions { + println!("{}", action); + } + return Ok(()); + } + // perform actions let db = get_db()?; let mut imap_session = connect(host, port, user, password)?; println!("getting capabilities.."); @@ -41,41 +52,13 @@ fn sync( println!("{:?}", x); names.push(x); } - - let mut remote = HashMap::new(); - - for &name in &names { - let mailbox = name.name(); - // if the user specified some mailboxes, only process those - if !mailboxes.is_empty() && !mailboxes.contains(&mailbox) { - continue; - } - println!("indexing {}", mailbox); - let resp = imap_session.examine(mailbox)?; - let uid_validity = resp.uid_validity.unwrap(); - - let mut mails = HashMap::new(); - let messages = imap_session.uid_fetch("1:*", "(FLAGS BODY[HEADER.FIELDS (MESSAGE-ID)])")?; - for m in messages.iter() { - let id = MaildirID::new(uid_validity, m.uid.unwrap()); - let flags = m.flags(); - if flags.contains(&Flag::Deleted) { - continue; - } - let header = m.header().unwrap(); - let mut message_id = parse_header(header).map(|x| x.0.get_value()).unwrap_or_default(); - if message_id.is_empty() { - message_id = fallback_mid(mailbox, id); - } - let flags = flags.iter().map(|x| remove_cow(x)).collect_vec(); - mails.insert(message_id, (id.uid_validity, id.uid, id, flags)); - } - remote.insert(mailbox, mails); + let trash_dir = names.iter().filter(|x| x.attributes().iter().any(|x| *x == TRASH)).map(|x| x.name()).next(); + if trash_dir.is_none() { + println!("Warning: unable to trash mail, no trash folder found!"); } let mut have_mail = db.prepare("SELECT mailbox, uid, flags FROM mail WHERE message_id = ?")?; let mut delete_mail = db.prepare("DELETE FROM mail WHERE mailbox = ? AND uid = ?")?; - let mut all_mail = db.prepare("SELECT uid, message_id, flags FROM mail WHERE mailbox = ?")?; let mut save_mail = db.prepare("INSERT INTO mail VALUES (?,?,?,?)")?; let mut maildirs: HashMap = names.iter().map(|&x| (x.name().to_owned(), get_maildir(x.name()).unwrap())).collect(); macro_rules! ensure_mailbox { @@ -86,125 +69,141 @@ fn sync( &maildirs[$name] }} } - let mut printed_trash_warning = false; - let trash_dir = names.iter().filter(|x| x.attributes().iter().any(|x| *x == TRASH)).map(|x| x.name()).next(); - let mut to_remove: HashMap<&str, _> = HashMap::new(); - for &name in &names { - let mailbox = name.name(); - // if the user specified some mailboxes, only process those - if !mailboxes.is_empty() && !mailboxes.contains(&mailbox) { - continue; + let mut selection = None; + + for action in actions { + let mut uid_valid = None; + if let Some(mailbox) = action.mailbox() { + if selection.is_none() || selection.as_ref().unwrap() != mailbox { + if selection.is_some() { + println!("expunging.."); + imap_session.expunge().context("expunge failed")?; + } + println!("selecting {}", mailbox); + uid_valid = imap_session.select(mailbox).context("select failed")?.uid_validity; + selection = Some(mailbox.to_string()); + } } - let is_trash = name.attributes().iter().any(|x| *x == TRASH); - let remote_mails = remote.get_mut(mailbox).unwrap(); - println!("selecting {}", mailbox); - imap_session.select(mailbox).context("select failed")?; - let all_mails = all_mail.query_map(params![mailbox], map3rows::)?; - let mut deleted_some = false; - for x in all_mails { - let (uid, mid, flags) = x?; - let uid: MaildirID = uid.into(); - if flags.contains(TRASHED) && !is_trash { + macro_rules! check_valid { + ($uid_validity:expr) => { + if uid_valid.is_none() || $uid_validity != uid_valid.unwrap() { + println!("Warning: uid validity value changed, unable to process action!"); + continue; + } + } + } + macro_rules! update_flags { + ($mailbox:expr, $id:expr, $remote_flags:expr, $flags:expr) => { + let local_s = $flags.contains('S'); + let local_u = $flags.contains(UNREAD); + let remote_s = $remote_flags.contains(&Flag::Seen); + if local_s && !remote_s { + println!("setting Seen flag on {}/{}", $mailbox, $id.uid); + imap_session.uid_store($id.to_imap(), "+FLAGS.SILENT (\\Seen)")?; + $remote_flags.push(Flag::Seen); + } else if local_u && remote_s { + println!("removing Seen flag on {}/{}", $mailbox, $id.uid); + imap_session.uid_store($id.to_imap(), "-FLAGS.SILENT (\\Seen)")?; + $remote_flags.remove($remote_flags.iter().position(|x| x == &Flag::Seen).unwrap()); + } + } + } + match action { + TrashRemote(mailbox, id) => { + check_valid!(id.uid_validity); if let Some(trash_dir) = trash_dir { - println!("trashing: {}/{}", mailbox, uid); - if remote_mails.contains_key(&mid) { - imap_session.uid_mv(uid.to_imap(), trash_dir)?; - } else { - println!("Warning: only trashing locally!"); - } + println!("trashing: {}/{}", mailbox, id.uid); + imap_session.uid_mv(id.to_imap(), trash_dir)?; let gone = ensure_mailbox!(".gone"); - let uid_name = uid.to_string(); - let _ = maildir_cp(&maildirs[mailbox], gone, &uid_name, &uid_name, "", true); - maildirs[mailbox].delete(&uid_name)?; - delete_mail.execute(params![mailbox, uid])?; - } else if !printed_trash_warning { - println!("Warning: unable to trash mail, no trash folder found!"); - printed_trash_warning = true; + let uid_name = id.to_string(); + let _ = maildir_cp(&maildirs[&mailbox], gone, &uid_name, &uid_name, "", true); + maildirs[&mailbox].delete(&uid_name)?; + delete_mail.execute(params![mailbox, id])?; } - } else if flags.contains(DELETE) { - println!("deleting: {}/{}", mailbox, uid); - if remote_mails.contains_key(&mid) { - imap_session.uid_store(uid.to_imap(), "+FLAGS.SILENT (\\Deleted)")?; - } else { - println!("Warning: only deleting locally!"); + }, + TrashLocal(mailbox, id) => { + check_valid!(id.uid_validity); + println!("trashing: {}/{}", mailbox, id.uid); + let gone = ensure_mailbox!(".gone"); + let uid_name = id.to_string(); + let _ = maildir_cp(&maildirs[&mailbox], gone, &uid_name, &uid_name, "", true); + maildirs[&mailbox].delete(&uid_name)?; + delete_mail.execute(params![mailbox, id])?; + }, + DeleteRemote(mailbox, id) => { + imap_session.uid_store(id.to_imap(), "+FLAGS.SILENT (\\Deleted)")?; + delete_mail.execute(params![mailbox, id])?; + maildirs[&mailbox].delete(&id.to_string())?; + }, + DeleteLocal(mailbox, id) => { + delete_mail.execute(params![mailbox, id])?; + maildirs[&mailbox].delete(&id.to_string())?; + }, + UpdateFlags(mailbox, mut ids) => { + for (id, remote_flags, flags) in &mut ids { + check_valid!(id.uid_validity); + update_flags!(mailbox, id, remote_flags, flags); } - remote_mails.remove(&mid); - delete_mail.execute(params![mailbox, uid])?; - maildirs[mailbox].delete(&uid.to_string())?; - deleted_some = true; - } - } - if deleted_some { - imap_session.expunge().context("expunge failed")?; - } + }, + Hardlink(mailbox, mut ids) => { + for (new_uid, message_id, remote_flags) in &mut ids { + check_valid!(new_uid.uid_validity); + let local = have_mail.query_map(params![&*message_id], map3rows::)?.map(|x| x.unwrap()).collect_vec(); + let (inbox, full_uid, flags) = &local[0]; + let local_id = full_uid.to_string(); + let new_id = new_uid.to_string(); + // hardlink mail + let maildir1 = ensure_mailbox!(inbox.as_str()); + let maildir2 = &maildirs[&mailbox]; + println!("hardlinking: {}/{} -> {}/{}", inbox, local_id, mailbox, new_id); + maildir_cp(maildir1, maildir2, &local_id, &new_id, flags, false)?; + save_mail.execute(params![mailbox, &*new_uid, &*message_id, flags])?; + update_flags!(mailbox, new_uid, remote_flags, flags); + } + }, + Fetch(mailbox, to_fetch) => { + let maildir = ensure_mailbox!(&mailbox); + check_valid!(to_fetch[0].uid_validity); - let mut to_fetch = Vec::new(); - for (message_id, entry) in remote_mails.iter_mut() { - let (uid1, uid2, full_uid, remote_flags) = entry; - let local = have_mail.query_map(params![message_id], map3rows::)?.map(|x| x.unwrap()).collect_vec(); - macro_rules! update_flags { - ($id:expr, $flags:expr) => { - let local_s = $flags.contains('S'); - let local_u = $flags.contains(UNREAD); - let remote_s = remote_flags.contains(&Flag::Seen); - if local_s && !remote_s { - println!("setting Seen flag on {}/{}", mailbox, $id.uid); - imap_session.uid_store($id.to_imap(), "+FLAGS.SILENT (\\Seen)")?; - remote_flags.push(Flag::Seen); - } else if local_u && remote_s { - println!("removing Seen flag on {}/{}", mailbox, $id.uid); - imap_session.uid_store($id.to_imap(), "-FLAGS.SILENT (\\Seen)")?; - remote_flags.remove(remote_flags.iter().position(|x| x == &Flag::Seen).unwrap()); + let fetch_range = to_fetch.into_iter().map(|x| x.uid.to_string()).join(","); + let fetch = imap_session.uid_fetch(fetch_range, "RFC822")?; + + for mail in fetch.iter() { + println!("fetching: {}/{}", mailbox, mail.uid.unwrap()); + let id = MaildirID::new(uid_valid.unwrap(), mail.uid.unwrap()); + let id_name = id.to_string(); + if !maildir.exists(&id_name) { + let mail_data = mail.body().unwrap_or_default(); + let flags = imap_flags_to_maildir("".into(), mail.flags()); + maildir.store_cur_with_id_flags(&id_name, &flags, mail_data)?; + + let headers = parse_headers(&mail_data)?.0; + let message_id = headers.message_id(&mailbox, id); + save_mail.execute(params![mailbox, id.to_i64(), message_id, flags])?; + } else { + println!("warning: DB outdated, downloaded mail again"); } } - } - if let Some((_, full_uid, flags)) = local.iter().filter(|x| x.0 == mailbox && x.1 == *full_uid).next() { - update_flags!(full_uid, flags); - continue; - } - if !local.is_empty() { - let (inbox, full_uid, flags) = &local[0]; - let local_id = full_uid.to_string(); - let new_uid = MaildirID::new(*uid1, *uid2); - let new_id = new_uid.to_string(); - // hardlink mail - let maildir1 = ensure_mailbox!(inbox.as_str()); - let maildir2 = &maildirs[mailbox]; - println!("hardlinking: {}/{} -> {}/{}", inbox, local_id, mailbox, new_id); - maildir_cp(maildir1, maildir2, &local_id, &new_id, flags, false)?; - save_mail.execute(params![mailbox, new_uid.to_i64(), message_id, flags])?; - update_flags!(new_uid, flags); - } else if !is_trash { // do not fetch trashed mail - println!("fetching {:?} {:?} as it is not in {:?}", uid2, message_id, local); - to_fetch.push(uid2); - } - } - if !to_fetch.is_empty() { - let resp = imap_session.examine(mailbox)?; - let uid_validity = resp.uid_validity.unwrap(); - let maildir = &maildirs[mailbox]; - - let fetch_range = to_fetch.into_iter().map(|x| x.to_string()).join(","); - let fetch = imap_session.uid_fetch(fetch_range, "RFC822")?; - - for mail in fetch.iter() { - println!("fetching: {}/{}", mailbox, mail.uid.unwrap()); - let id = MaildirID::new(uid_validity, mail.uid.unwrap()); - let id_name = id.to_string(); - if !maildir.exists(&id_name) { - let mail_data = mail.body().unwrap_or_default(); - let flags = imap_flags_to_maildir("".into(), mail.flags()); - maildir.store_cur_with_id_flags(&id_name, &flags, mail_data)?; - - let headers = parse_headers(&mail_data)?.0; - let message_id = headers.message_id(mailbox, id); - save_mail.execute(params![mailbox, id.to_i64(), message_id, flags])?; - } else { - println!("warning: DB outdated, downloaded mail again"); + }, + RemoveStale(to_remove) => { + for mailbox in to_remove.keys() { + for &(uid1, uid2, uid) in &to_remove[&*mailbox] { + let uid_name = gen_id(uid1, uid2); + println!("removing: {}/{}", mailbox, uid_name); + let gone = ensure_mailbox!(".gone"); + let maildir = &maildirs[&*mailbox]; + // hardlink should only fail if the mail was already deleted + let _ = maildir_cp(maildir, gone, &uid_name, &uid_name, "", true); + maildir.delete(&uid_name)?; + delete_mail.execute(params![mailbox, store_i64(uid)])?; + } } - } + }, } - let maildir = &maildirs[mailbox]; + } + // final flag update + for (mailbox, remote_mails) in remote { + let maildir = ensure_mailbox!(&mailbox); for message_id in remote_mails.keys() { let (uid1, uid2, _, ref flags) = remote_mails[message_id]; let id = gen_id(uid1, uid2); @@ -214,43 +213,6 @@ fn sync( Maildir::normalize_flags(&f) }); } - let mails = all_mail.query_map(params![mailbox], |row| - Ok((load_i64(row.get::<_, i64>(0)?), row.get::<_, String>(1)?)))? - .map(|x| x.unwrap()).collect_vec(); - let mut removed = Vec::new(); - for (uid, message_id) in mails { - let uid1 = (uid >> 32) as u32; - let uid2 = ((uid << 32) >> 32) as u32; - if !remote_mails.contains_key(&message_id) && !message_id.ends_with("@no-message-id>") { - removed.push((uid1, uid2, uid)); - } - } - if !removed.is_empty() { - to_remove.insert(mailbox, removed); - } } - for &mailbox in to_remove.keys() { - for &(uid1, uid2, uid) in &to_remove[mailbox] { - let uid_name = gen_id(uid1, uid2); - println!("removing: {}/{}", mailbox, uid_name); - let gone = ensure_mailbox!(".gone"); - let maildir = &maildirs[mailbox]; - // hardlink should only fail if the mail was already deleted - let _ = maildir_cp(maildir, gone, &uid_name, &uid_name, "", true); - maildir.delete(&uid_name)?; - delete_mail.execute(params![mailbox, store_i64(uid)])?; - } - } - - // be nice to the server and log out - imap_session.logout()?; - Ok(()) } - -pub fn map3rows(row: &Row) -> rusqlite::Result<(A, B, C)> { - let a = row.get::<_, A>(0)?; - let b = row.get::<_, B>(1)?; - let c = row.get::<_, C>(2)?; - Ok((a, b, c)) -}