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 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<String, Maildir> = 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)])?;
}

View File

@ -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<StreamOwned<ClientSession, TcpStream>>;
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<ImapSession> {
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 {
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<char> {
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<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
}