mirror of
https://github.com/FliegendeWurst/inboxid.git
synced 2024-11-21 16:34:59 +00:00
browse: style trashed and deleted mail
This commit is contained in:
parent
07d5862e16
commit
14bc4d15c8
@ -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)])?;
|
||||
}
|
||||
|
88
src/lib.rs
88
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<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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user