diff --git a/src/bin/rebuild-db.rs b/src/bin/rebuild-db.rs index 1906213..74cbcef 100644 --- a/src/bin/rebuild-db.rs +++ b/src/bin/rebuild-db.rs @@ -6,24 +6,34 @@ use mailparse::MailHeaderMap; use rusqlite::params; fn main() -> Result<()> { - let db = get_db()?; - let mut delete_mail = db.prepare("DELETE FROM mail WHERE mailbox = ?")?; - let mut save_mail = db.prepare("INSERT INTO mail VALUES (?,?,?)")?; + let mut db = get_db()?; + let tx = db.transaction()?; + { + let mut delete_mail = tx.prepare("DELETE FROM mail WHERE mailbox = ?")?; + let mut save_mail = tx.prepare("INSERT INTO mail VALUES (?,?,?,?)")?; let mailboxes = env::args().skip(1).collect_vec(); for mailbox in mailboxes { + println!("reading {}..", mailbox); let maildir = get_maildir(&mailbox)?; delete_mail.execute(params![&mailbox])?; let mut mails = Vec::new(); for x in maildir.list_cur() { mails.push(x?); } + println!("acquired {} mails", mails.len()); let mut mails = maildir.get_mails(&mut mails)?; mails.sort_by_key(|x| x.date); for mail in mails { let headers = mail.get_headers(); - let message_id = headers.get_all_values("Message-ID").join(" "); - save_mail.execute(params![&mailbox, mail.id.to_i64(), message_id])?; + let mut message_id = headers.get_all_values("Message-ID").join(" "); + if message_id.is_empty() { + message_id = format!("<{}_{}_{}@no-message-id>", mailbox, mail.id.uid_validity, mail.id.uid); + } + save_mail.execute(params![&mailbox, mail.id.to_i64(), message_id, mail.flags])?; } } + } + tx.commit()?; + db.execute("VACUUM", params![])?; Ok(()) } diff --git a/src/bin/sync.rs b/src/bin/sync.rs index d0ea789..7888c8d 100644 --- a/src/bin/sync.rs +++ b/src/bin/sync.rs @@ -64,45 +64,60 @@ fn sync( remote.insert(mailbox, mails); } - let mut have_mail = db.prepare("SELECT mailbox, uid 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 all_mail = db.prepare("SELECT uid, message_id 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<&str, Maildir> = names.iter().map(|&x| (x.name(), get_maildir(x.name()).unwrap())).collect(); let mut to_remove: HashMap<&str, _> = HashMap::new(); for &name in &names { let mailbox = name.name(); - let remote_mails = &remote[mailbox]; + let remote_mails = remote.get_mut(mailbox).unwrap(); + let resp = imap_session.examine(mailbox)?; + let uid_validity = resp.uid_validity.unwrap(); let mut to_fetch = Vec::new(); - for message_id in remote_mails.keys() { - let (uid1, uid2, full_uid, ref _flags) = remote_mails[message_id]; - let local = have_mail.query_map(params![message_id], |row| Ok((row.get::<_, String>(0)?, load_i64(row.get::<_, i64>(1)?))))?.map(|x| x.unwrap()).collect_vec(); - if local.iter().any(|x| x.0 == mailbox && x.1 == full_uid) { + 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], |row| Ok(( + row.get::<_, String>(0)?, + load_i64(row.get::<_, i64>(1)?), + row.get::<_, String>(2)? + )))?.map(|x| x.unwrap()).collect_vec(); + if let Some((_, full_uid, flags)) = local.iter().filter(|x| x.0 == mailbox && x.1 == *full_uid).next() { + let uid = (full_uid << 32) >> 32; + let local_s = flags.contains('S'); + let local_u = flags.contains('U'); + let remote_s = remote_flags.contains(&Flag::Seen); + if local_s && !remote_s { + imap_session.uid_store(uid.to_string(), "+FLAGS.SILENT (\\Seen)")?; + remote_flags.push(Flag::Seen); + } else if local_u && remote_s { + imap_session.uid_store(uid.to_string(), "-FLAGS.SILENT (\\Seen)")?; + remote_flags.remove(remote_flags.iter().position(|x| x == &Flag::Seen).unwrap()); + } continue; } if !local.is_empty() { - let (inbox, full_uid) = &local[0]; + let (inbox, full_uid, flags) = &local[0]; let local_uid1 = (full_uid >> 32) as u32; let local_uid2 = ((full_uid << 32) >> 32) as u32; let local_id = gen_id(local_uid1, local_uid2); - let new_uid = MaildirID::new(uid1, uid2); + let new_uid = MaildirID::new(*uid1, *uid2); let new_id = new_uid.to_string(); // hardlink mail let maildir1 = &maildirs[&**inbox]; println!("hardlinking: {}/{} -> {}/{}", inbox, local_id, mailbox, new_id); let name = maildir1.find_filename(&local_id).unwrap(); let maildir2 = &maildirs[mailbox]; - maildir2.store_cur_from_path(&new_id, name)?; - save_mail.execute(params![mailbox, new_uid.to_i64(), message_id])?; + maildir2.store_cur_from_path(&new_id, flags, name)?; + save_mail.execute(params![mailbox, new_uid.to_i64(), message_id, flags])?; } else if !name.attributes().iter().any(|x| *x == TRASH) { // do not fetch trashed mail to_fetch.push(uid2); } } if !to_fetch.is_empty() { let maildir = &maildirs[mailbox]; - let resp = imap_session.examine(mailbox)?; - let uid_validity = resp.uid_validity.unwrap(); let fetch_range = to_fetch.into_iter().map(|x| x.to_string()).join(","); let fetch = imap_session.uid_fetch(fetch_range, "RFC822")?; @@ -113,7 +128,8 @@ fn sync( let id = gen_id(uid_validity, uid); if !maildir.exists(&id) { let mail_data = mail.body().unwrap_or_default(); - maildir.store_cur_with_id(&id, mail_data)?; + let flags = imap_flags_to_maildir("".into(), mail.flags()); + maildir.store_cur_with_id_flags(&id, &flags, mail_data)?; let headers = parse_headers(&mail_data)?.0; let mut message_id = headers.get_all_values("Message-ID").join(" "); @@ -121,7 +137,7 @@ fn sync( message_id = format!("<{}_{}_{}@no-message-id>", mailbox, uid_validity, uid); } let full_uid = ((uid_validity as u64) << 32) | uid as u64; - save_mail.execute(params![mailbox, store_i64(full_uid), message_id])?; + save_mail.execute(params![mailbox, store_i64(full_uid), message_id, flags])?; } else { println!("warning: DB outdated, downloaded mail again"); } @@ -132,22 +148,8 @@ fn sync( let (uid1, uid2, _, ref flags) = remote_mails[message_id]; let id = gen_id(uid1, uid2); let _ = maildir.update_flags(&id, |f| { - let mut f = f.to_owned(); - if flags.contains(&Flag::Seen) { - f.push('S'); - } else { - f = f.replace('S', ""); - } - if flags.contains(&Flag::Answered) { - f.push('R'); - } else { - f = f.replace('R', ""); - } - if flags.contains(&Flag::Flagged) { - f.push('F'); - } else { - f = f.replace('F', ""); - } + let f = f.replace('U', ""); + let f = imap_flags_to_maildir(f, flags); Maildir::normalize_flags(&f) }); } @@ -175,7 +177,8 @@ fn sync( } let maildir = &maildirs[mailbox]; let name = maildir.find_filename(&uid_name).unwrap(); - maildirs[".gone"].store_new_from_path(&format!("{}_{}", mailbox, uid_name), name)?; + let _ = maildirs[".gone"].store_new_from_path(&format!("{}_{}", mailbox, uid_name), name); + // hardlink should only fail if the mail was already deleted maildir.delete(&uid_name)?; delete_mail.execute(params![mailbox, store_i64(uid)])?; } diff --git a/src/lib.rs b/src/lib.rs index a3df0bb..31c62e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,8 @@ pub fn get_db() -> Result { CREATE TABLE IF NOT EXISTS mail( mailbox STRING NOT NULL, uid INTEGER NOT NULL, - message_id STRING NOT NULL + message_id STRING NOT NULL, + flags STRING NOT NULL )", params![])?; Ok(conn) @@ -64,7 +65,7 @@ pub fn gen_id(uid_validity: u32, uid: u32) -> String { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct MaildirID { - uid_validity: u32, + pub uid_validity: u32, pub uid: u32, } @@ -105,7 +106,8 @@ pub struct EasyMail<'a> { mail: Option>, pub id: MaildirID, pub flags: String, - from: SingleInfo, + from: Option, + from_raw: String, pub subject: String, pub date: DateTime, pub date_iso: String, @@ -117,10 +119,8 @@ impl EasyMail<'_> { mail: None, id: MaildirID::new(0, 0), flags: "S".to_owned(), - from: SingleInfo { - display_name: None, - addr: String::new() - }, + from: None, + from_raw: String::new(), subject, date: Local.from_utc_datetime(&NaiveDateTime::from_timestamp(0, 0)), date_iso: "????-??-??".to_owned() @@ -132,13 +132,17 @@ impl EasyMail<'_> { } pub fn from(&self) -> String { - let name = self.from.display_name.as_deref().unwrap_or_default(); - if let Some(config) = CONFIG.get() { - if config.read().browse.show_email_addresses { - return format!("{} <{}>", name, self.from.addr); + if let Some(from) = self.from.as_ref() { + let name = from.display_name.as_deref().unwrap_or_default(); + if let Some(config) = CONFIG.get() { + if config.read().browse.show_email_addresses { + return format!("{} <{}>", name, from.addr); + } } + name.to_owned() + } else { + self.from_raw.clone() } - name.to_owned() } pub fn get_header(&self, header: &str) -> String { @@ -173,8 +177,10 @@ impl Eq for EasyMail<'_> {} impl Hash for EasyMail<'_> { fn hash(&self, state: &mut H) { self.id.hash(state); - self.from.display_name.hash(state); - self.from.addr.hash(state); + if let Some(from) = self.from.as_ref() { + from.display_name.hash(state); + from.addr.hash(state); + } self.subject.hash(state); } } @@ -326,7 +332,8 @@ impl MaildirExtension for Maildir { let flags = maile.flags().to_owned(); let mail = maile.parsed()?; let headers = mail.get_headers(); - let from = addrparse(&headers.get_all_values("From").join(" "))?.extract_single_info().context("failed to extract from")?; + let from_raw = headers.get_all_values("From").join(" "); + let from = addrparse(&from_raw).map(|x| x.extract_single_info()).ok().flatten(); let subject = headers.get_all_values("Subject").join(" "); let date = headers.get_all_values("Date").join(" "); let date = dateparse(&date).map(|x| @@ -337,6 +344,7 @@ impl MaildirExtension for Maildir { flags, id, from, + from_raw, subject, date_iso: date.format("%Y-%m-%d %H:%M").to_string(), date, @@ -353,7 +361,8 @@ impl MaildirExtension for Maildir { let flags = maile.flags().to_owned(); let mail = maile.parsed()?; let headers = mail.get_headers(); - let from = addrparse(&headers.get_all_values("From").join(" "))?.extract_single_info().context("failed to extract from")?; + let from_raw = headers.get_all_values("From").join(" "); + let from = addrparse(&from_raw).map(|x| x.extract_single_info()).ok().flatten(); let subject = headers.get_all_values("Subject").join(" "); let date = headers.get_all_values("Date").join(" "); let date = dateparse(&date).map(|x| @@ -364,6 +373,7 @@ impl MaildirExtension for Maildir { flags, id, from, + from_raw, subject, date_iso: date.format("%Y-%m-%d %H:%M").to_string(), date, @@ -534,3 +544,22 @@ pub fn parse_effect(effect: &str) -> Option { fn default_unread_style() -> Style { Effect::Reverse.into() } + +pub fn imap_flags_to_maildir(mut f: String, flags: &[Flag]) -> String { + if flags.contains(&Flag::Seen) { + f.push('S'); + } else { + f = f.replace('S', ""); + } + if flags.contains(&Flag::Answered) { + f.push('R'); + } else { + f = f.replace('R', ""); + } + if flags.contains(&Flag::Flagged) { + f.push('F'); + } else { + f = f.replace('F', ""); + } + f +}