From 14bc4d15c80a9277d5203c7aca513160df8dec05 Mon Sep 17 00:00:00 2001 From: FliegendeWurst <2012gdwu+github@posteo.de> Date: Mon, 12 Apr 2021 12:40:10 +0200 Subject: [PATCH] browse: style trashed and deleted mail --- src/bin/sync.rs | 45 ++++++++++++------------- src/lib.rs | 88 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 28 deletions(-) diff --git a/src/bin/sync.rs b/src/bin/sync.rs index 75ddbc9..981caac 100644 --- a/src/bin/sync.rs +++ b/src/bin/sync.rs @@ -6,7 +6,7 @@ use itertools::Itertools; use maildir::Maildir; use inboxid::*; -use mailparse::{MailHeaderMap, parse_header, parse_headers}; +use mailparse::{parse_header, parse_headers}; use rusqlite::{Row, params, types::FromSql}; const TRASH: NameAttribute = NameAttribute::Custom(Cow::Borrowed("\\Trash")); @@ -16,8 +16,10 @@ fn main() -> Result<()> { 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(); - sync(&host, &user, &password, port) + sync(&host, &user, &password, port, &args) } fn sync( @@ -25,6 +27,7 @@ fn sync( user: &str, password: &str, port: u16, + mailboxes: &[&str] ) -> Result<()> { let db = get_db()?; let mut imap_session = connect(host, port, user, password)?; @@ -68,11 +71,11 @@ fn sync( 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<&str, Maildir> = names.iter().map(|&x| (x.name(), get_maildir(x.name()).unwrap())).collect(); + let mut maildirs: HashMap = names.iter().map(|&x| (x.name().to_owned(), get_maildir(x.name()).unwrap())).collect(); macro_rules! ensure_mailbox { ($name:expr) => {{ if !maildirs.contains_key($name) { - maildirs.insert($name, get_maildir($name)?); + maildirs.insert($name.to_owned(), get_maildir($name)?); } &maildirs[$name] }} @@ -82,6 +85,10 @@ fn sync( 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 is_trash = name.attributes().iter().any(|x| *x == TRASH); let remote_mails = remote.get_mut(mailbox).unwrap(); println!("selecting {}", mailbox); @@ -101,9 +108,9 @@ fn sync( } let gone = ensure_mailbox!(".gone"); let uid_name = uid.to_string(); - let _ = maildir_cp(&maildirs[mailbox], gone, &uid_name, &uid_name); + let _ = maildir_cp(&maildirs[mailbox], gone, &uid_name, &uid_name, "", true); maildirs[mailbox].delete(&uid_name)?; - delete_mail.execute(params![mailbox, uid.to_i64()])?; + 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; @@ -116,7 +123,7 @@ fn sync( println!("Warning: only deleting locally!"); } remote_mails.remove(&mid); - delete_mail.execute(params![mailbox, uid.to_i64()])?; + delete_mail.execute(params![mailbox, uid])?; maildirs[mailbox].delete(&uid.to_string())?; deleted_some = true; } @@ -162,11 +169,10 @@ fn sync( 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 maildir1 = ensure_mailbox!(inbox.as_str()); let maildir2 = &maildirs[mailbox]; - maildir2.store_cur_from_path(&new_id, flags, name)?; + 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.to_u64(), flags); } else if !is_trash { // do not fetch trashed mail @@ -182,9 +188,8 @@ fn sync( let fetch = imap_session.uid_fetch(fetch_range, "RFC822")?; for mail in fetch.iter() { - let uid = mail.uid.unwrap(); - println!("fetching: {}/{}", mailbox, uid); - let id = MaildirID::new(uid_validity, uid); + 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(); @@ -192,12 +197,8 @@ fn sync( maildir.store_cur_with_id_flags(&id_name, &flags, mail_data)?; let headers = parse_headers(&mail_data)?.0; - let mut message_id = headers.get_all_values("Message-ID").join(" "); - if message_id.is_empty() { - message_id = headers.message_id(mailbox, id); - } - let full_uid = ((uid_validity as u64) << 32) | uid as u64; - save_mail.execute(params![mailbox, store_i64(full_uid), message_id, flags])?; + 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"); } @@ -228,14 +229,14 @@ fn sync( to_remove.insert(mailbox, removed); } } - for mailbox in to_remove.keys() { + 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); + let _ = maildir_cp(maildir, gone, &uid_name, &uid_name, "", true); maildir.delete(&uid_name)?; delete_mail.execute(params![mailbox, store_i64(uid)])?; } diff --git a/src/lib.rs b/src/lib.rs index 1799691..6e2f2c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, convert::{TryFrom, TryInto}, env, fmt::{Debug, Display}, use anyhow::Context; use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; -use cursive::{theme::{Effect, Style}, utils::span::{IndexedCow, IndexedSpan, SpannedString}}; +use cursive::{theme::{BaseColor, Color, ColorStyle, ColorType, Effect, Style}, utils::span::{IndexedCow, IndexedSpan, SpannedString}}; use cursive_tree_view::TreeEntry; use directories_next::ProjectDirs; use imap::{Session, types::Flag}; @@ -12,7 +12,7 @@ use mailparse::{MailHeaderMap, ParsedMail, SingleInfo, addrparse, dateparse}; use once_cell::sync::OnceCell; use parking_lot::RwLock; use petgraph::{Graph, graph::NodeIndex}; -use rusqlite::{Connection, params}; +use rusqlite::{Connection, ToSql, params, types::ToSqlOutput}; use rustls_connector::{RustlsConnector, rustls::{ClientSession, StreamOwned}}; use serde::{Deserializer, Serializer}; use serde::de::Visitor; @@ -24,6 +24,9 @@ pub type ImapSession = Session>; pub const UNREAD: char = 'U'; pub const TRASHED: char = 'T'; pub const DELETE: char = 'E'; // Exterminate +pub const SEEN: char = 'S'; +pub const REPLIED: char = 'R'; +pub const FLAGGED: char = 'F'; pub fn connect(host: &str, port: u16, user: &str, password: &str) -> Result { println!("connecting.."); @@ -94,6 +97,12 @@ impl From for MaildirID { } } +impl ToSql for MaildirID { + fn to_sql(&self) -> rusqlite::Result> { + Ok(ToSqlOutput::from(self.to_i64())) + } +} + impl Display for MaildirID { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}_{}", self.uid_validity, self.uid) @@ -121,9 +130,13 @@ impl MaildirID { } } -pub fn maildir_cp(maildir1: &Maildir, maildir2: &Maildir, id1: &str, id2: &str) -> Result<()> { +pub fn maildir_cp(maildir1: &Maildir, maildir2: &Maildir, id1: &str, id2: &str, flags: &str, new: bool) -> Result<()> { let name = maildir1.find_filename(id1).context("mail not found")?; - maildir2.store_new_from_path(id2, name)?; + if new { + maildir2.store_new_from_path(id2, name)?; + } else { + maildir2.store_cur_from_path(id2, flags, name)?; + } Ok(()) } @@ -174,6 +187,10 @@ impl EasyMail<'_> { self.flags.read().contains(imap_flag_to_maildir(flag).unwrap()) } + pub fn has_flag2(&self, flag: char) -> bool { + self.flags.read().contains(flag) + } + pub fn add_flag(&self, flag: Flag) { self.flags.write().push(imap_flag_to_maildir(&flag).unwrap()); } @@ -278,7 +295,15 @@ impl TreeEntry for &EasyMail<'_> { line.push(' '); line += &self.date_iso; - let style = if self.has_flag(&Flag::Seen) { Style::default() } else { CONFIG.get().unwrap().read().browse.unread_style }; + let style = if self.has_flag2(DELETE) { + CONFIG.get().unwrap().read().browse.deleted_style + } else if self.has_flag(&Flag::Deleted) { + CONFIG.get().unwrap().read().browse.trashed_style + } else if !self.has_flag(&Flag::Seen) { + CONFIG.get().unwrap().read().browse.unread_style + } else { + Style::default() + }; let spans = vec![ IndexedSpan { content: IndexedCow::Borrowed { @@ -571,13 +596,23 @@ pub struct Browse { #[serde(deserialize_with = "deserialize_style")] #[serde(serialize_with = "serialize_style")] pub unread_style: Style, + #[serde(default = "default_trashed_style")] + #[serde(deserialize_with = "deserialize_style")] + #[serde(serialize_with = "serialize_style")] + pub trashed_style: Style, + #[serde(default = "default_deleted_style")] + #[serde(deserialize_with = "deserialize_style")] + #[serde(serialize_with = "serialize_style")] + pub deleted_style: Style, } impl Default for Browse { fn default() -> Self { Self { show_email_addresses: Default::default(), - unread_style: default_unread_style() + unread_style: default_unread_style(), + trashed_style: default_trashed_style(), + deleted_style: default_deleted_style() } } } @@ -635,6 +670,16 @@ fn default_unread_style() -> Style { Effect::Reverse.into() } +fn default_trashed_style() -> Style { + let mut color = ColorStyle::primary(); + color.front = ColorType::Color(Color::Light(BaseColor::Black)); + color.into() +} + +fn default_deleted_style() -> Style { + Effect::Strikethrough.into() +} + pub fn imap_flags_to_maildir(mut f: String, flags: &[Flag]) -> String { if flags.contains(&Flag::Seen) { f.push('S'); @@ -658,6 +703,37 @@ pub fn imap_flag_to_maildir(flag: &Flag) -> Option { match flag { Flag::Seen => Some('S'), Flag::Answered => Some('R'), + Flag::Flagged => Some('F'), + Flag::Deleted => Some('T'), _ => None } } + +pub fn maildir_flags_to_imap(flags: &str) -> Vec { + let mut x = vec![]; + for c in flags.chars() { + if let Some(f) = match c { + REPLIED => Some(Flag::Answered), + SEEN => Some(Flag::Seen), + FLAGGED => Some(Flag::Flagged), + TRASHED => Some(Flag::Deleted), + _ => None + } { + x.push(f); + } + } + x +} + +pub fn imap_flags_to_cmd(flags: &[Flag]) -> String { + let mut x = "(".to_owned(); + for f in flags { + x += &f.to_string(); + x.push(' '); + } + if x.ends_with(' ') { + x.pop(); + } + x.push(')'); + x +}