mirror of
https://github.com/FliegendeWurst/inboxid.git
synced 2024-11-09 10:50:40 +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 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)])?;
|
||||||
}
|
}
|
||||||
|
86
src/lib.rs
86
src/lib.rs
@ -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")?;
|
||||||
|
if new {
|
||||||
maildir2.store_new_from_path(id2, name)?;
|
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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user