diff --git a/Cargo.lock b/Cargo.lock index 181a135..6e8a718 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,32 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-executor" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +dependencies = [ + "async-lock 3.2.0", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite 2.1.0", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "blocking", + "futures-lite 1.13.0", +] + [[package]] name = "async-io" version = "1.13.0" @@ -390,6 +416,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -834,9 +866,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" dependencies = [ "cfg-if", ] @@ -1096,6 +1128,16 @@ dependencies = [ "dirs-sys 0.4.1", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -1119,6 +1161,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dunce" version = "1.0.4" @@ -1193,8 +1246,7 @@ dependencies = [ [[package]] name = "email-lib" version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743371f76482a94403ce0ab49da129065fc84cbcf9ed126524882c6ed5389efc" +source = "git+https://git.sr.ht/~soywod/pimalaya#8fb9b5ecc417a34a824a6decc3c0cda01af98ffe" dependencies = [ "advisory-lock", "anyhow", @@ -1213,6 +1265,8 @@ dependencies = [ "maildirpp", "md5", "mml-lib", + "notify", + "notify-rust", "notmuch", "oauth-lib", "once_cell", @@ -1546,7 +1600,10 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" dependencies = [ + "fastrand 2.0.1", "futures-core", + "futures-io", + "parking", "pin-project-lite", ] @@ -2090,7 +2147,7 @@ dependencies = [ "dirs 4.0.0", "gix-path", "libc", - "windows", + "windows 0.43.0", ] [[package]] @@ -2629,6 +2686,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inout" version = "0.1.3" @@ -2748,6 +2825,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2855,6 +2952,19 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "mac-notification-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fca4d74ff9dbaac16a01b924bc3693fa2bba0862c2c633abc73f9a8ea21f64" +dependencies = [ + "cc", + "dirs-next", + "objc-foundation", + "objc_id", + "time", +] + [[package]] name = "mail-auth" version = "0.3.6" @@ -2925,6 +3035,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -3008,6 +3127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -3063,6 +3183,36 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.4.1", + "filetime", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify-rust" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "827c5edfa80235ded4ab3fe8e9dc619b4f866ef16fe9b1c6b8a7f8692c0f2226" +dependencies = [ + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "notmuch" version = "0.8.0" @@ -3238,6 +3388,35 @@ dependencies = [ "url", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.32.1" @@ -4545,6 +4724,16 @@ version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" +[[package]] +name = "tauri-winrt-notification" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006851c9ccefa3c38a7646b8cec804bb429def3da10497bfa977179869c3e8e2" +dependencies = [ + "quick-xml", + "windows 0.51.1", +] + [[package]] name = "tempfile" version = "3.8.1" @@ -5152,6 +5341,16 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core", + "windows-targets 0.48.5", +] + [[package]] name = "windows-core" version = "0.51.1" @@ -5422,9 +5621,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948" dependencies = [ "async-broadcast", + "async-executor", + "async-fs", + "async-io 1.13.0", + "async-lock 2.8.0", "async-process", "async-recursion", + "async-task", "async-trait", + "blocking", "byteorder", "derivative", "enumflags2", diff --git a/Cargo.toml b/Cargo.toml index c4f8bc9..35d6224 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ clap_mangen = "0.2" console = "0.15.2" dialoguer = "0.10.2" dirs = "4.0" -email-lib = { version = "=0.17.1", default-features = false } +email-lib = { git = "https://git.sr.ht/~soywod/pimalaya", default-features = false } email_address = "0.2.4" env_logger = "0.8" erased-serde = "0.3" diff --git a/config.sample.toml b/config.sample.toml index ea15584..e2ab591 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -44,6 +44,13 @@ envelope.list.backend = "imap" # Override the backend used for sending messages. message.send.backend = "smtp" +# Send notification when receiving new messages +message.watch.received.notify.summary = "📬 New message from {sender}" +message.watch.received.notify.body = "{subject}" + +# Shell commands can also be executed +# message.watch.received.cmd = "mbsync -a" + # IMAP config imap.host = "localhost" imap.port = 3143 diff --git a/src/account/config.rs b/src/account/config.rs index 389700b..294b7e1 100644 --- a/src/account/config.rs +++ b/src/account/config.rs @@ -191,6 +191,14 @@ impl TomlAccountConfig { .or_else(|| self.backend.as_ref()) } + pub fn get_watch_message_kind(&self) -> Option<&BackendKind> { + self.message + .as_ref() + .and_then(|msg| msg.watch.as_ref()) + .and_then(|watch| watch.backend.as_ref()) + .or_else(|| self.backend.as_ref()) + } + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { let mut used_backends = HashSet::default(); diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 05efa54..9e80afd 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -11,6 +11,7 @@ use email::imap::{ImapSessionBuilder, ImapSessionSync}; use email::smtp::{SmtpClientBuilder, SmtpClientSync}; use email::{ account::config::AccountConfig, + email::watch::{imap::WatchImapEmails, maildir::WatchMaildirEmails}, envelope::{ get::{imap::GetEnvelopeImap, maildir::GetEnvelopeMaildir}, list::{imap::ListEnvelopesImap, maildir::ListEnvelopesMaildir}, @@ -355,6 +356,33 @@ impl BackendBuilder { _ => (), } + match toml_account_config.backend { + Some(BackendKind::Maildir) => { + backend_builder = backend_builder.with_watch_emails(|ctx| { + ctx.maildir.as_ref().and_then(WatchMaildirEmails::new) + }); + } + Some(BackendKind::MaildirForSync) => { + backend_builder = backend_builder.with_watch_emails(|ctx| { + ctx.maildir_for_sync + .as_ref() + .and_then(WatchMaildirEmails::new) + }); + } + #[cfg(feature = "imap")] + Some(BackendKind::Imap) => { + backend_builder = backend_builder + .with_watch_emails(|ctx| ctx.imap.as_ref().and_then(WatchImapEmails::new)); + } + #[cfg(feature = "notmuch")] + Some(BackendKind::Notmuch) => { + backend_builder = backend_builder.with_watch_emails(|ctx| { + ctx.notmuch.as_ref().and_then(WatchNotmuchEmails::new) + }); + } + _ => (), + } + match toml_account_config.get_envelope_kind() { Some(BackendKind::Maildir) => { backend_builder = backend_builder.with_get_envelope(|ctx| { diff --git a/src/config/mod.rs b/src/config/mod.rs index 9fafd1f..619a531 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -211,6 +211,7 @@ impl TomlConfig { read: c.read.map(|c| c.remote), write: c.write.map(|c| c.remote), send: c.send.map(|c| c.remote), + watch: c.watch.map(|c| c.remote), }), sync: config.sync, #[cfg(feature = "pgp")] diff --git a/src/email/message/config.rs b/src/email/message/config.rs index 4d95ef4..f8a1338 100644 --- a/src/email/message/config.rs +++ b/src/email/message/config.rs @@ -12,6 +12,8 @@ pub struct MessageConfig { pub copy: Option, #[serde(rename = "move")] pub move_: Option, + + pub watch: Option, } impl MessageConfig { @@ -42,6 +44,10 @@ impl MessageConfig { kinds.extend(move_.get_used_backends()); } + if let Some(watch) = &self.watch { + kinds.extend(watch.get_used_backends()); + } + kinds } } @@ -156,3 +162,23 @@ impl MessageMoveConfig { kinds } } + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +pub struct WatchMessageConfig { + pub backend: Option, + + #[serde(flatten)] + pub remote: email::message::watch::config::WatchMessageConfig, +} + +impl WatchMessageConfig { + pub fn get_used_backends(&self) -> HashSet<&BackendKind> { + let mut kinds = HashSet::default(); + + if let Some(kind) = &self.backend { + kinds.insert(kind); + } + + kinds + } +} diff --git a/src/folder/command/mod.rs b/src/folder/command/mod.rs index 5de0450..ede0d31 100644 --- a/src/folder/command/mod.rs +++ b/src/folder/command/mod.rs @@ -3,6 +3,7 @@ mod delete; mod expunge; mod list; mod purge; +mod watch; use anyhow::Result; use clap::Subcommand; @@ -11,7 +12,7 @@ use crate::{config::TomlConfig, printer::Printer}; use self::{ create::FolderCreateCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand, - list::FolderListCommand, purge::FolderPurgeCommand, + list::FolderListCommand, purge::FolderPurgeCommand, watch::FolderWatchCommand, }; /// Manage folders. @@ -26,6 +27,9 @@ pub enum FolderSubcommand { #[command(alias = "lst")] List(FolderListCommand), + #[command()] + Watch(FolderWatchCommand), + #[command()] Expunge(FolderExpungeCommand), @@ -41,6 +45,7 @@ impl FolderSubcommand { match self { Self::Create(cmd) => cmd.execute(printer, config).await, Self::List(cmd) => cmd.execute(printer, config).await, + Self::Watch(cmd) => cmd.execute(printer, config).await, Self::Expunge(cmd) => cmd.execute(printer, config).await, Self::Purge(cmd) => cmd.execute(printer, config).await, Self::Delete(cmd) => cmd.execute(printer, config).await, diff --git a/src/folder/command/watch.rs b/src/folder/command/watch.rs new file mode 100644 index 0000000..dbeaf9b --- /dev/null +++ b/src/folder/command/watch.rs @@ -0,0 +1,42 @@ +use anyhow::Result; +use clap::Parser; +use log::info; + +use crate::{ + account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag, + config::TomlConfig, folder::arg::name::FolderNameArg, printer::Printer, +}; + +/// Watch a folder for changes. +/// +/// This command allows you to watch a new folder using the given +/// name. +#[derive(Debug, Parser)] +pub struct FolderWatchCommand { + #[command(flatten)] + pub folder: FolderNameArg, + + #[command(flatten)] + pub cache: CacheDisableFlag, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl FolderWatchCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing folder watch command"); + + let folder = &self.folder.name; + + let some_account_name = self.account.name.as_ref().map(String::as_str); + let (toml_account_config, account_config) = config + .clone() + .into_account_configs(some_account_name, self.cache.disable)?; + let backend = Backend::new(toml_account_config, account_config.clone(), false).await?; + + printer.print_log(format!("Start watching folder {folder} for changes…"))?; + + backend.watch_emails(&folder).await + } +}