From 30cc5e95e2a14f8fd14275fda2068a9622f5eda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sun, 6 Oct 2024 11:11:31 +0200 Subject: [PATCH] implement main loop commands --- Cargo.lock | 114 +++++++++++----------- src/backend.rs | 89 +++++++++++++++++ src/editor.rs | 215 +++++++++++++++++++++++++++++++++++++++++ src/envelope.rs | 26 +---- src/main.rs | 248 ++++++++++++++++++++++++++++++++++++++++-------- 5 files changed, 567 insertions(+), 125 deletions(-) create mode 100644 src/editor.rs diff --git a/Cargo.lock b/Cargo.lock index f72f998..f821f01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,9 +510,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.22" +version = "1.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9540e661f81799159abee814118cc139a2004b3a3aa3ea37724a1b66530b90e0" +checksum = "677207f6eaec43fcfd092a718c847fc38aa261d0e19b8ef6797e0ccbe789e738" dependencies = [ "shlex", ] @@ -563,7 +563,7 @@ version = "1.0.0-alpha.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7b80276986f86789dc56ca6542d53bba9cda3c66091ebbe7bd96fc1bdf20f1f" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "regex-automata 0.3.9", "serde", "unicode-ident", @@ -581,9 +581,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.18" +version = "4.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" dependencies = [ "clap_builder", "clap_derive", @@ -591,9 +591,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.18" +version = "4.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" dependencies = [ "anstream", "anstyle", @@ -1431,9 +1431,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -1446,9 +1446,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1456,15 +1456,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1473,9 +1473,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -1504,9 +1504,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -1515,21 +1515,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1701,6 +1701,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "heck" version = "0.5.0" @@ -1905,9 +1911,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -1986,7 +1992,7 @@ dependencies = [ "hyper 1.4.1", "hyper-util", "log", - "rustls 0.23.13", + "rustls 0.23.14", "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", @@ -2102,7 +2108,7 @@ dependencies = [ [[package]] name = "imap-next" version = "0.2.0" -source = "git+https://github.com/duesee/imap-next#75671ca68e067e82a8846bef0e9396809ca93ffa" +source = "git+https://github.com/duesee/imap-next#7e120f40cb30cef0f761c0efd44a4846234e9e91" dependencies = [ "bytes", "imap-codec", @@ -2134,12 +2140,12 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.0", ] [[package]] @@ -2223,9 +2229,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is_terminal_polyfill" @@ -2433,7 +2439,7 @@ checksum = "7a575d25cf00ed68e5790b473b29242a47e991c6187785d47b45e31fc5816554" dependencies = [ "base64 0.22.1", "gethostname", - "rustls 0.23.13", + "rustls 0.23.14", "rustls-pki-types", "smtp-proto", "tokio", @@ -2814,12 +2820,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" -dependencies = [ - "portable-atomic", -] +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl-probe" @@ -3072,7 +3075,7 @@ dependencies = [ [[package]] name = "pimalaya-tui" version = "0.1.0" -source = "git+https://github.com/pimalaya/tui#b40ca9d9e161b91e9efcb762fa05f06e3fe46f63" +source = "git+https://github.com/pimalaya/tui#c565ecdee52593451f02261f5e206b86372bef16" dependencies = [ "clap", "color-eyre", @@ -3176,12 +3179,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "portable-atomic" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" - [[package]] name = "ppv-lite86" version = "0.2.20" @@ -3590,9 +3587,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "log", "once_cell", @@ -3622,7 +3619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.3", + "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", "security-framework", @@ -3639,11 +3636,10 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] @@ -4214,12 +4210,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" dependencies = [ "rustix 0.38.37", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -4312,7 +4308,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.13", + "rustls 0.23.14", "rustls-pki-types", "tokio", ] @@ -4500,9 +4496,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" diff --git a/src/backend.rs b/src/backend.rs index ea62c1b..c27e7da 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -14,7 +14,12 @@ use email::{ context::BackendContextBuilder, feature::BackendFeature, macros::BackendContext, mapper::SomeBackendContextBuilderMapper, }, + envelope::list::ListEnvelopes, folder::list::ListFolders, + message::{ + add::AddMessage, copy::CopyMessages, delete::DeleteMessages, get::GetMessages, + r#move::MoveMessages, send::SendMessage, + }, AnyResult, }; use serde::{Deserialize, Serialize}; @@ -131,6 +136,88 @@ impl BackendContextBuilder for ContextBuilder { } } + fn list_envelopes(&self) -> Option> { + match self.backend { + #[cfg(feature = "imap")] + BackendKind::Imap => self.list_envelopes_with_some(&self.imap), + #[cfg(feature = "maildir")] + BackendKind::Maildir => self.list_envelopes_with_some(&self.maildir), + #[cfg(feature = "notmuch")] + BackendKind::Notmuch => self.list_envelopes_with_some(&self.notmuch), + _ => None, + } + } + + fn get_messages(&self) -> Option> { + match self.backend { + #[cfg(feature = "imap")] + BackendKind::Imap => self.get_messages_with_some(&self.imap), + #[cfg(feature = "maildir")] + BackendKind::Maildir => self.get_messages_with_some(&self.maildir), + #[cfg(feature = "notmuch")] + BackendKind::Notmuch => self.get_messages_with_some(&self.notmuch), + _ => None, + } + } + + fn add_message(&self) -> Option> { + match self.backend { + #[cfg(feature = "imap")] + BackendKind::Imap => self.add_message_with_some(&self.imap), + #[cfg(feature = "maildir")] + BackendKind::Maildir => self.add_message_with_some(&self.maildir), + #[cfg(feature = "notmuch")] + BackendKind::Notmuch => self.add_message_with_some(&self.notmuch), + _ => None, + } + } + + fn send_message(&self) -> Option> { + match self.sending_backend { + #[cfg(feature = "smtp")] + BackendKind::Smtp => self.send_message_with_some(&self.smtp), + #[cfg(feature = "sendmail")] + BackendKind::Sendmail => self.send_message_with_some(&self.sendmail), + _ => None, + } + } + + fn copy_messages(&self) -> Option> { + match self.backend { + #[cfg(feature = "imap")] + BackendKind::Imap => self.copy_messages_with_some(&self.imap), + #[cfg(feature = "maildir")] + BackendKind::Maildir => self.copy_messages_with_some(&self.maildir), + #[cfg(feature = "notmuch")] + BackendKind::Notmuch => self.copy_messages_with_some(&self.notmuch), + _ => None, + } + } + + fn move_messages(&self) -> Option> { + match self.backend { + #[cfg(feature = "imap")] + BackendKind::Imap => self.move_messages_with_some(&self.imap), + #[cfg(feature = "maildir")] + BackendKind::Maildir => self.move_messages_with_some(&self.maildir), + #[cfg(feature = "notmuch")] + BackendKind::Notmuch => self.move_messages_with_some(&self.notmuch), + _ => None, + } + } + + fn delete_messages(&self) -> Option> { + match self.backend { + #[cfg(feature = "imap")] + BackendKind::Imap => self.delete_messages_with_some(&self.imap), + #[cfg(feature = "maildir")] + BackendKind::Maildir => self.delete_messages_with_some(&self.maildir), + #[cfg(feature = "notmuch")] + BackendKind::Notmuch => self.delete_messages_with_some(&self.notmuch), + _ => None, + } + } + async fn build(self) -> AnyResult { #[cfg(feature = "imap")] let imap = match self.imap { @@ -176,3 +263,5 @@ impl BackendContextBuilder for ContextBuilder { }) } } + +pub type Backend = email::backend::Backend; diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 0000000..7b1a644 --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,215 @@ +use std::{env, fmt, fs, sync::Arc}; + +use color_eyre::{eyre::Context, Result}; +use email::{ + account::config::AccountConfig, + flag::{Flag, Flags}, + folder::DRAFTS, + local_draft_path, + message::{add::AddMessage, send::SendMessageThenSaveCopy}, + remove_local_draft, + template::Template, +}; +use mml::MmlCompilerBuilder; +use pimalaya_tui::prompt; +use process::SingleCommand; + +use crate::backend::Backend; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PreEditChoice { + Edit, + Discard, + Quit, +} + +impl fmt::Display for PreEditChoice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Edit => "Edit it", + Self::Discard => "Discard it", + Self::Quit => "Quit", + } + ) + } +} + +static PRE_EDIT_CHOICES: [PreEditChoice; 3] = [ + PreEditChoice::Edit, + PreEditChoice::Discard, + PreEditChoice::Quit, +]; + +pub fn pre_edit() -> Result { + let user_choice = prompt::item( + "A draft was found, what would you like to do with it?", + &PRE_EDIT_CHOICES, + None, + )?; + + Ok(user_choice.clone()) +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PostEditChoice { + Send, + Edit, + LocalDraft, + RemoteDraft, + Discard, +} + +impl fmt::Display for PostEditChoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Send => "Send it", + Self::Edit => "Edit it again", + Self::LocalDraft => "Save it as local draft", + Self::RemoteDraft => "Save it as remote draft", + Self::Discard => "Discard it", + } + ) + } +} + +static POST_EDIT_CHOICES: [PostEditChoice; 5] = [ + PostEditChoice::Send, + PostEditChoice::Edit, + PostEditChoice::LocalDraft, + PostEditChoice::RemoteDraft, + PostEditChoice::Discard, +]; + +pub fn post_edit() -> Result { + let user_choice = prompt::item( + "What would you like to do with this message?", + &POST_EDIT_CHOICES, + None, + )?; + + Ok(user_choice.clone()) +} +pub async fn edit_tpl_with_editor( + config: Arc, + backend: &Backend, + mut tpl: Template, +) -> Result<()> { + let draft = local_draft_path(); + if draft.exists() { + loop { + match pre_edit() { + Ok(choice) => match choice { + PreEditChoice::Edit => { + tpl = open_with_local_draft().await?; + break; + } + PreEditChoice::Discard => { + tpl = open_with_tpl(tpl).await?; + break; + } + PreEditChoice::Quit => return Ok(()), + }, + Err(err) => { + println!("{}", err); + continue; + } + } + } + } else { + tpl = open_with_tpl(tpl).await?; + } + + loop { + match post_edit() { + Ok(PostEditChoice::Send) => { + println!("Sending email…"); + + #[allow(unused_mut)] + let mut compiler = MmlCompilerBuilder::new(); + + #[cfg(feature = "pgp")] + compiler.set_some_pgp(config.pgp.clone()); + + let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; + + backend.send_message_then_save_copy(&email).await?; + + remove_local_draft()?; + println!("Done!"); + break; + } + Ok(PostEditChoice::Edit) => { + tpl = open_with_tpl(tpl).await?; + continue; + } + Ok(PostEditChoice::LocalDraft) => { + println!("Email successfully saved locally"); + break; + } + Ok(PostEditChoice::RemoteDraft) => { + #[allow(unused_mut)] + let mut compiler = MmlCompilerBuilder::new(); + + #[cfg(feature = "pgp")] + compiler.set_some_pgp(config.pgp.clone()); + + let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; + + backend + .add_message_with_flags( + DRAFTS, + &email, + &Flags::from_iter([Flag::Seen, Flag::Draft]), + ) + .await?; + remove_local_draft()?; + println!("Email successfully saved to drafts"); + break; + } + Ok(PostEditChoice::Discard) => { + remove_local_draft()?; + break; + } + Err(err) => { + println!("{}", err); + continue; + } + } + } + + Ok(()) +} + +pub async fn open_with_local_draft() -> Result