browse: style trashed and deleted mail

This commit is contained in:
FliegendeWurst 2021-04-12 12:40:10 +02:00 committed by Arne Keller
parent 07d5862e16
commit 14bc4d15c8
2 changed files with 105 additions and 28 deletions

View File

@ -6,7 +6,7 @@ use itertools::Itertools;
use maildir::Maildir; use maildir::Maildir;
use inboxid::*; use inboxid::*;
use mailparse::{MailHeaderMap, parse_header, parse_headers}; use mailparse::{parse_header, parse_headers};
use rusqlite::{Row, params, types::FromSql}; use rusqlite::{Row, params, types::FromSql};
const TRASH: NameAttribute = NameAttribute::Custom(Cow::Borrowed("\\Trash")); 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 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 args = args.iter().map(|x| &**x).collect_vec();
sync(&host, &user, &password, port) sync(&host, &user, &password, port, &args)
} }
fn sync( fn sync(
@ -25,6 +27,7 @@ fn sync(
user: &str, user: &str,
password: &str, password: &str,
port: u16, port: u16,
mailboxes: &[&str]
) -> Result<()> { ) -> Result<()> {
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)?;
@ -68,11 +71,11 @@ fn sync(
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 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<&str, Maildir> = names.iter().map(|&x| (x.name(), 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 {
($name:expr) => {{ ($name:expr) => {{
if !maildirs.contains_key($name) { if !maildirs.contains_key($name) {
maildirs.insert($name, get_maildir($name)?); maildirs.insert($name.to_owned(), get_maildir($name)?);
} }
&maildirs[$name] &maildirs[$name]
}} }}
@ -82,6 +85,10 @@ fn sync(
let mut to_remove: HashMap<&str, _> = HashMap::new(); let mut to_remove: HashMap<&str, _> = HashMap::new();
for &name in &names { for &name in &names {
let mailbox = name.name(); 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 is_trash = name.attributes().iter().any(|x| *x == TRASH);
let remote_mails = remote.get_mut(mailbox).unwrap(); let remote_mails = remote.get_mut(mailbox).unwrap();
println!("selecting {}", mailbox); println!("selecting {}", mailbox);
@ -101,9 +108,9 @@ fn sync(
} }
let gone = ensure_mailbox!(".gone"); let gone = ensure_mailbox!(".gone");
let uid_name = uid.to_string(); 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)?; maildirs[mailbox].delete(&uid_name)?;
delete_mail.execute(params![mailbox, uid.to_i64()])?; delete_mail.execute(params![mailbox, uid])?;
} else if !printed_trash_warning { } else if !printed_trash_warning {
println!("Warning: unable to trash mail, no trash folder found!"); println!("Warning: unable to trash mail, no trash folder found!");
printed_trash_warning = true; printed_trash_warning = true;
@ -116,7 +123,7 @@ fn sync(
println!("Warning: only deleting locally!"); println!("Warning: only deleting locally!");
} }
remote_mails.remove(&mid); remote_mails.remove(&mid);
delete_mail.execute(params![mailbox, uid.to_i64()])?; delete_mail.execute(params![mailbox, uid])?;
maildirs[mailbox].delete(&uid.to_string())?; maildirs[mailbox].delete(&uid.to_string())?;
deleted_some = true; deleted_some = true;
} }
@ -162,11 +169,10 @@ fn sync(
let new_uid = MaildirID::new(*uid1, *uid2); 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 = &maildirs[&**inbox]; let maildir1 = ensure_mailbox!(inbox.as_str());
println!("hardlinking: {}/{} -> {}/{}", inbox, local_id, mailbox, new_id);
let name = maildir1.find_filename(&local_id).unwrap();
let maildir2 = &maildirs[mailbox]; 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])?; save_mail.execute(params![mailbox, new_uid.to_i64(), message_id, flags])?;
update_flags!(new_uid.to_u64(), flags); update_flags!(new_uid.to_u64(), flags);
} else if !is_trash { // do not fetch trashed mail } else if !is_trash { // do not fetch trashed mail
@ -182,9 +188,8 @@ fn sync(
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() {
let uid = mail.uid.unwrap(); println!("fetching: {}/{}", mailbox, mail.uid.unwrap());
println!("fetching: {}/{}", mailbox, uid); let id = MaildirID::new(uid_validity, mail.uid.unwrap());
let id = MaildirID::new(uid_validity, uid);
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();
@ -192,12 +197,8 @@ 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 mut message_id = headers.get_all_values("Message-ID").join(" "); let message_id = headers.message_id(mailbox, id);
if message_id.is_empty() { save_mail.execute(params![mailbox, id.to_i64(), message_id, flags])?;
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])?;
} else { } else {
println!("warning: DB outdated, downloaded mail again"); println!("warning: DB outdated, downloaded mail again");
} }
@ -228,14 +229,14 @@ fn sync(
to_remove.insert(mailbox, removed); 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] { for &(uid1, uid2, uid) in &to_remove[mailbox] {
let uid_name = gen_id(uid1, uid2); let uid_name = gen_id(uid1, uid2);
println!("removing: {}/{}", mailbox, uid_name); println!("removing: {}/{}", mailbox, uid_name);
let gone = ensure_mailbox!(".gone"); let gone = ensure_mailbox!(".gone");
let maildir = &maildirs[mailbox]; let maildir = &maildirs[mailbox];
// hardlink should only fail if the mail was already deleted // 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)?; maildir.delete(&uid_name)?;
delete_mail.execute(params![mailbox, store_i64(uid)])?; delete_mail.execute(params![mailbox, store_i64(uid)])?;
} }

View File

@ -2,7 +2,7 @@ use std::{borrow::Cow, convert::{TryFrom, TryInto}, env, fmt::{Debug, Display},
use anyhow::Context; use anyhow::Context;
use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; 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 cursive_tree_view::TreeEntry;
use directories_next::ProjectDirs; use directories_next::ProjectDirs;
use imap::{Session, types::Flag}; use imap::{Session, types::Flag};
@ -12,7 +12,7 @@ use mailparse::{MailHeaderMap, ParsedMail, SingleInfo, addrparse, dateparse};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use parking_lot::RwLock; use parking_lot::RwLock;
use petgraph::{Graph, graph::NodeIndex}; use petgraph::{Graph, graph::NodeIndex};
use rusqlite::{Connection, params}; use rusqlite::{Connection, ToSql, params, types::ToSqlOutput};
use rustls_connector::{RustlsConnector, rustls::{ClientSession, StreamOwned}}; use rustls_connector::{RustlsConnector, rustls::{ClientSession, StreamOwned}};
use serde::{Deserializer, Serializer}; use serde::{Deserializer, Serializer};
use serde::de::Visitor; use serde::de::Visitor;
@ -24,6 +24,9 @@ pub type ImapSession = Session<StreamOwned<ClientSession, TcpStream>>;
pub const UNREAD: char = 'U'; pub const UNREAD: char = 'U';
pub const TRASHED: char = 'T'; pub const TRASHED: char = 'T';
pub const DELETE: char = 'E'; // Exterminate 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<ImapSession> { pub fn connect(host: &str, port: u16, user: &str, password: &str) -> Result<ImapSession> {
println!("connecting.."); println!("connecting..");
@ -94,6 +97,12 @@ impl From<i64> for MaildirID {
} }
} }
impl ToSql for MaildirID {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'static>> {
Ok(ToSqlOutput::from(self.to_i64()))
}
}
impl Display for MaildirID { impl Display for MaildirID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}_{}", self.uid_validity, self.uid) 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")?; 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(()) Ok(())
} }
@ -174,6 +187,10 @@ impl EasyMail<'_> {
self.flags.read().contains(imap_flag_to_maildir(flag).unwrap()) 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) { pub fn add_flag(&self, flag: Flag) {
self.flags.write().push(imap_flag_to_maildir(&flag).unwrap()); self.flags.write().push(imap_flag_to_maildir(&flag).unwrap());
} }
@ -278,7 +295,15 @@ impl TreeEntry for &EasyMail<'_> {
line.push(' '); line.push(' ');
line += &self.date_iso; 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![ let spans = vec![
IndexedSpan { IndexedSpan {
content: IndexedCow::Borrowed { content: IndexedCow::Borrowed {
@ -571,13 +596,23 @@ pub struct Browse {
#[serde(deserialize_with = "deserialize_style")] #[serde(deserialize_with = "deserialize_style")]
#[serde(serialize_with = "serialize_style")] #[serde(serialize_with = "serialize_style")]
pub unread_style: 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 { impl Default for Browse {
fn default() -> Self { fn default() -> Self {
Self { Self {
show_email_addresses: Default::default(), 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() 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 { pub fn imap_flags_to_maildir(mut f: String, flags: &[Flag]) -> String {
if flags.contains(&Flag::Seen) { if flags.contains(&Flag::Seen) {
f.push('S'); f.push('S');
@ -658,6 +703,37 @@ pub fn imap_flag_to_maildir(flag: &Flag) -> Option<char> {
match flag { match flag {
Flag::Seen => Some('S'), Flag::Seen => Some('S'),
Flag::Answered => Some('R'), Flag::Answered => Some('R'),
Flag::Flagged => Some('F'),
Flag::Deleted => Some('T'),
_ => None _ => None
} }
} }
pub fn maildir_flags_to_imap(flags: &str) -> Vec<Flag> {
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
}