mirror of
https://github.com/FliegendeWurst/inboxid.git
synced 2024-11-22 00:45:01 +00:00
sync: refactor to action architecture
this makes for an easy --dry-run implementation
This commit is contained in:
parent
1f2d329549
commit
6d277d588a
219
inboxid-sync/src/lib.rs
Normal file
219
inboxid-sync/src/lib.rs
Normal file
@ -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<Flag<'static>>, String)>),
|
||||||
|
Hardlink(String, Vec<(MaildirID, String, Vec<Flag<'static>>)>),
|
||||||
|
Fetch(String, Vec<MaildirID>),
|
||||||
|
RemoveStale(HashMap<String, Vec<(u32, u32, u64)>>)
|
||||||
|
}
|
||||||
|
|
||||||
|
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<SyncAction>, HashMap<String, HashMap<String, (u32, u32, MaildirID, Vec<Flag<'static>>)>>)> {
|
||||||
|
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<String, Maildir> = 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<String, _> = 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::<i64, String, String>)?;
|
||||||
|
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::<String, MaildirID, String>)?.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<A: FromSql, B: FromSql, C: FromSql>(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))
|
||||||
|
}
|
@ -1,25 +1,27 @@
|
|||||||
use std::{borrow::Cow, collections::HashMap, env};
|
use std::{env, collections::HashMap};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use imap::types::{Flag, NameAttribute};
|
use imap::types::Flag;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use maildir::Maildir;
|
|
||||||
|
|
||||||
use inboxid_lib::*;
|
use inboxid_lib::*;
|
||||||
use mailparse::{parse_header, parse_headers};
|
use inboxid_sync::*;
|
||||||
use rusqlite::{Row, params, types::FromSql};
|
use inboxid_sync::SyncAction::*;
|
||||||
|
use maildir::Maildir;
|
||||||
const TRASH: NameAttribute = NameAttribute::Custom(Cow::Borrowed("\\Trash"));
|
use mailparse::parse_headers;
|
||||||
|
use rusqlite::params;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let host = env::var("MAILHOST").expect("missing envvar MAILHOST");
|
let host = env::var("MAILHOST").expect("missing envvar MAILHOST");
|
||||||
let user = env::var("MAILUSER").expect("missing envvar MAILUSER");
|
let user = env::var("MAILUSER").expect("missing envvar MAILUSER");
|
||||||
let password = env::var("MAILPASSWORD").expect("missing envvar MAILPASSWORD");
|
let password = env::var("MAILPASSWORD").expect("missing envvar MAILPASSWORD");
|
||||||
let port = 993;
|
let port = 993;
|
||||||
let args = env::args().skip(1).collect_vec();
|
let mut args = env::args().skip(1).peekable();
|
||||||
let args = args.iter().map(|x| &**x).collect_vec();
|
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(
|
fn sync(
|
||||||
@ -27,8 +29,17 @@ fn sync(
|
|||||||
user: &str,
|
user: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
port: u16,
|
port: u16,
|
||||||
mailboxes: &[&str]
|
mailboxes: &[String],
|
||||||
|
dry_run: bool
|
||||||
) -> Result<()> {
|
) -> 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 db = get_db()?;
|
||||||
let mut imap_session = connect(host, port, user, password)?;
|
let mut imap_session = connect(host, port, user, password)?;
|
||||||
println!("getting capabilities..");
|
println!("getting capabilities..");
|
||||||
@ -41,41 +52,13 @@ fn sync(
|
|||||||
println!("{:?}", x);
|
println!("{:?}", x);
|
||||||
names.push(x);
|
names.push(x);
|
||||||
}
|
}
|
||||||
|
let trash_dir = names.iter().filter(|x| x.attributes().iter().any(|x| *x == TRASH)).map(|x| x.name()).next();
|
||||||
let mut remote = HashMap::new();
|
if trash_dir.is_none() {
|
||||||
|
println!("Warning: unable to trash mail, no trash folder found!");
|
||||||
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 mut have_mail = db.prepare("SELECT mailbox, uid, flags FROM mail WHERE message_id = ?")?;
|
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 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 save_mail = db.prepare("INSERT INTO mail VALUES (?,?,?,?)")?;
|
||||||
let mut maildirs: HashMap<String, Maildir> = names.iter().map(|&x| (x.name().to_owned(), get_maildir(x.name()).unwrap())).collect();
|
let mut maildirs: HashMap<String, Maildir> = names.iter().map(|&x| (x.name().to_owned(), get_maildir(x.name()).unwrap())).collect();
|
||||||
macro_rules! ensure_mailbox {
|
macro_rules! ensure_mailbox {
|
||||||
@ -86,110 +69,108 @@ fn sync(
|
|||||||
&maildirs[$name]
|
&maildirs[$name]
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
let mut printed_trash_warning = false;
|
let mut selection = None;
|
||||||
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 action in actions {
|
||||||
for &name in &names {
|
let mut uid_valid = None;
|
||||||
let mailbox = name.name();
|
if let Some(mailbox) = action.mailbox() {
|
||||||
// if the user specified some mailboxes, only process those
|
if selection.is_none() || selection.as_ref().unwrap() != mailbox {
|
||||||
if !mailboxes.is_empty() && !mailboxes.contains(&mailbox) {
|
if selection.is_some() {
|
||||||
continue;
|
println!("expunging..");
|
||||||
}
|
|
||||||
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::<i64, String, String>)?;
|
|
||||||
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 {
|
|
||||||
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!");
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
} 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!");
|
|
||||||
}
|
|
||||||
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")?;
|
imap_session.expunge().context("expunge failed")?;
|
||||||
}
|
}
|
||||||
|
println!("selecting {}", mailbox);
|
||||||
let mut to_fetch = Vec::new();
|
uid_valid = imap_session.select(mailbox).context("select failed")?.uid_validity;
|
||||||
for (message_id, entry) in remote_mails.iter_mut() {
|
selection = Some(mailbox.to_string());
|
||||||
let (uid1, uid2, full_uid, remote_flags) = entry;
|
|
||||||
let local = have_mail.query_map(params![message_id], map3rows::<String, MaildirID, String>)?.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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
macro_rules! check_valid {
|
||||||
if let Some((_, full_uid, flags)) = local.iter().filter(|x| x.0 == mailbox && x.1 == *full_uid).next() {
|
($uid_validity:expr) => {
|
||||||
update_flags!(full_uid, flags);
|
if uid_valid.is_none() || $uid_validity != uid_valid.unwrap() {
|
||||||
|
println!("Warning: uid validity value changed, unable to process action!");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !local.is_empty() {
|
}
|
||||||
|
}
|
||||||
|
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, id.uid);
|
||||||
|
imap_session.uid_mv(id.to_imap(), trash_dir)?;
|
||||||
|
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])?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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::<String, MaildirID, String>)?.map(|x| x.unwrap()).collect_vec();
|
||||||
let (inbox, full_uid, flags) = &local[0];
|
let (inbox, full_uid, flags) = &local[0];
|
||||||
let local_id = full_uid.to_string();
|
let local_id = full_uid.to_string();
|
||||||
let new_uid = MaildirID::new(*uid1, *uid2);
|
|
||||||
let new_id = new_uid.to_string();
|
let new_id = new_uid.to_string();
|
||||||
// hardlink mail
|
// hardlink mail
|
||||||
let maildir1 = ensure_mailbox!(inbox.as_str());
|
let maildir1 = ensure_mailbox!(inbox.as_str());
|
||||||
let maildir2 = &maildirs[mailbox];
|
let maildir2 = &maildirs[&mailbox];
|
||||||
println!("hardlinking: {}/{} -> {}/{}", inbox, local_id, mailbox, new_id);
|
println!("hardlinking: {}/{} -> {}/{}", inbox, local_id, mailbox, new_id);
|
||||||
maildir_cp(maildir1, maildir2, &local_id, &new_id, flags, false)?;
|
maildir_cp(maildir1, maildir2, &local_id, &new_id, flags, false)?;
|
||||||
save_mail.execute(params![mailbox, new_uid.to_i64(), message_id, flags])?;
|
save_mail.execute(params![mailbox, &*new_uid, &*message_id, flags])?;
|
||||||
update_flags!(new_uid, flags);
|
update_flags!(mailbox, new_uid, remote_flags, 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() {
|
Fetch(mailbox, to_fetch) => {
|
||||||
let resp = imap_session.examine(mailbox)?;
|
let maildir = ensure_mailbox!(&mailbox);
|
||||||
let uid_validity = resp.uid_validity.unwrap();
|
check_valid!(to_fetch[0].uid_validity);
|
||||||
let maildir = &maildirs[mailbox];
|
|
||||||
|
|
||||||
let fetch_range = to_fetch.into_iter().map(|x| x.to_string()).join(",");
|
let fetch_range = to_fetch.into_iter().map(|x| x.uid.to_string()).join(",");
|
||||||
let fetch = imap_session.uid_fetch(fetch_range, "RFC822")?;
|
let fetch = imap_session.uid_fetch(fetch_range, "RFC822")?;
|
||||||
|
|
||||||
for mail in fetch.iter() {
|
for mail in fetch.iter() {
|
||||||
println!("fetching: {}/{}", mailbox, mail.uid.unwrap());
|
println!("fetching: {}/{}", mailbox, mail.uid.unwrap());
|
||||||
let id = MaildirID::new(uid_validity, mail.uid.unwrap());
|
let id = MaildirID::new(uid_valid.unwrap(), mail.uid.unwrap());
|
||||||
let id_name = id.to_string();
|
let id_name = id.to_string();
|
||||||
if !maildir.exists(&id_name) {
|
if !maildir.exists(&id_name) {
|
||||||
let mail_data = mail.body().unwrap_or_default();
|
let mail_data = mail.body().unwrap_or_default();
|
||||||
@ -197,14 +178,32 @@ fn sync(
|
|||||||
maildir.store_cur_with_id_flags(&id_name, &flags, mail_data)?;
|
maildir.store_cur_with_id_flags(&id_name, &flags, mail_data)?;
|
||||||
|
|
||||||
let headers = parse_headers(&mail_data)?.0;
|
let headers = parse_headers(&mail_data)?.0;
|
||||||
let message_id = headers.message_id(mailbox, id);
|
let message_id = headers.message_id(&mailbox, id);
|
||||||
save_mail.execute(params![mailbox, id.to_i64(), message_id, flags])?;
|
save_mail.execute(params![mailbox, id.to_i64(), message_id, flags])?;
|
||||||
} else {
|
} else {
|
||||||
println!("warning: DB outdated, downloaded mail again");
|
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() {
|
for message_id in remote_mails.keys() {
|
||||||
let (uid1, uid2, _, ref flags) = remote_mails[message_id];
|
let (uid1, uid2, _, ref flags) = remote_mails[message_id];
|
||||||
let id = gen_id(uid1, uid2);
|
let id = gen_id(uid1, uid2);
|
||||||
@ -214,43 +213,6 @@ fn sync(
|
|||||||
Maildir::normalize_flags(&f)
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn map3rows<A: FromSql, B: FromSql, C: FromSql>(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))
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user