diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d04c3d..9c659e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- SMTP pre-send hook [#178] + ### Changed - Improve `attachments` command [#281] @@ -453,6 +457,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#162]: https://github.com/soywod/himalaya/issues/162 [#176]: https://github.com/soywod/himalaya/issues/176 [#172]: https://github.com/soywod/himalaya/issues/172 +[#178]: https://github.com/soywod/himalaya/issues/178 [#181]: https://github.com/soywod/himalaya/issues/181 [#185]: https://github.com/soywod/himalaya/issues/185 [#186]: https://github.com/soywod/himalaya/issues/186 diff --git a/src/config/account_config.rs b/src/config/account_config.rs index ed01a30..d821d0c 100644 --- a/src/config/account_config.rs +++ b/src/config/account_config.rs @@ -36,6 +36,9 @@ pub struct AccountConfig { /// Represents mailbox aliases. pub mailboxes: HashMap, + /// Represents hooks. + pub hooks: Hooks, + /// Represents the SMTP host. pub smtp_host: String, /// Represents the SMTP port. @@ -155,6 +158,7 @@ impl<'a> AccountConfig { .to_owned(), format: base_account.format.unwrap_or_default(), mailboxes: base_account.mailboxes.clone(), + hooks: base_account.hooks.unwrap_or_default(), default: base_account.default.unwrap_or_default(), email: base_account.email.to_owned(), @@ -203,8 +207,7 @@ impl<'a> AccountConfig { /// Builds the full RFC822 compliant address of the user account. pub fn address(&self) -> Result { - let has_special_chars = - "()<>[]:;@.,".contains(|special_char| self.display_name.contains(special_char)); + let has_special_chars = "()<>[]:;@.,".contains(|c| self.display_name.contains(c)); let addr = if self.display_name.is_empty() { self.email.clone() } else if has_special_chars { diff --git a/src/config/deserialized_account_config.rs b/src/config/deserialized_account_config.rs index 3164ab1..84e7dff 100644 --- a/src/config/deserialized_account_config.rs +++ b/src/config/deserialized_account_config.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use std::{collections::HashMap, path::PathBuf}; -use crate::config::Format; +use crate::config::{Format, Hooks}; pub trait ToDeserializedBaseAccountConfig { fn to_base(&self) -> DeserializedBaseAccountConfig; @@ -84,6 +84,9 @@ macro_rules! make_account_config { #[serde(default)] pub mailboxes: HashMap, + /// Represents hooks. + pub hooks: Option, + $(pub $element: $ty),* } @@ -114,6 +117,7 @@ macro_rules! make_account_config { pgp_decrypt_cmd: self.pgp_decrypt_cmd.clone(), mailboxes: self.mailboxes.clone(), + hooks: self.hooks.clone(), } } } diff --git a/src/config/hooks.rs b/src/config/hooks.rs new file mode 100644 index 0000000..4bd44f0 --- /dev/null +++ b/src/config/hooks.rs @@ -0,0 +1,7 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Hooks { + pub pre_send: Option, +} diff --git a/src/lib.rs b/src/lib.rs index 3994874..32384d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,6 +126,9 @@ pub mod config { pub mod format; pub use format::*; + + pub mod hooks; + pub use hooks::*; } pub mod compl; diff --git a/src/msg/msg_entity.rs b/src/msg/msg_entity.rs index 85a8d49..edbc867 100644 --- a/src/msg/msg_entity.rs +++ b/src/msg/msg_entity.rs @@ -24,7 +24,7 @@ use crate::{ }; /// Representation of a message. -#[derive(Debug, Default)] +#[derive(Debug, Clone, Default)] pub struct Msg { /// The sequence number of the message. /// @@ -359,15 +359,18 @@ impl Msg { loop { match choice::post_edit() { Ok(PostEditChoice::Send) => { - let sent_msg = smtp.send_msg(account, &self)?; + printer.print_str("Sending message…")?; + let sent_msg = smtp.send(account, &self)?; let sent_folder = account .mailboxes .get("sent") .map(|s| s.as_str()) .unwrap_or(DEFAULT_SENT_FOLDER); - backend.add_msg(&sent_folder, &sent_msg.formatted(), "seen")?; + printer + .print_str(format!("Adding message to the {:?} folder…", sent_folder))?; + backend.add_msg(&sent_folder, &sent_msg, "seen")?; msg_utils::remove_local_draft()?; - printer.print_struct("Message successfully sent")?; + printer.print_struct("Done!")?; break; } Ok(PostEditChoice::Edit) => { @@ -711,7 +714,19 @@ impl TryInto for Msg { type Error = Error; fn try_into(self) -> Result { - let from = match self.from.and_then(|addrs| addrs.extract_single_info()) { + (&self).try_into() + } +} + +impl TryInto for &Msg { + type Error = Error; + + fn try_into(self) -> Result { + let from = match self + .from + .as_ref() + .and_then(|addrs| addrs.clone().extract_single_info()) + { Some(addr) => addr.addr.parse().map(Some), None => Ok(None), }?; diff --git a/src/msg/msg_handlers.rs b/src/msg/msg_handlers.rs index 6357d35..d58e737 100644 --- a/src/msg/msg_handlers.rs +++ b/src/msg/msg_handlers.rs @@ -8,7 +8,6 @@ use log::{debug, info, trace}; use mailparse::addrparse; use std::{ borrow::Cow, - convert::TryInto, fs, io::{self, BufRead}, }; @@ -356,9 +355,8 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( .join("\r\n") }; trace!("raw message: {:?}", raw_msg); - let envelope: lettre::address::Envelope = Msg::from_tpl(&raw_msg)?.try_into()?; - trace!("envelope: {:?}", envelope); - smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?; + let msg = Msg::from_tpl(&raw_msg)?; + smtp.send(&config, &msg)?; backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?; Ok(()) } diff --git a/src/msg/tpl_handlers.rs b/src/msg/tpl_handlers.rs index 712f426..b2db225 100644 --- a/src/msg/tpl_handlers.rs +++ b/src/msg/tpl_handlers.rs @@ -103,7 +103,7 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( .join("\n") }; let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?; - let sent_msg = smtp.send_msg(account, &msg)?; - backend.add_msg(mbox, &sent_msg.formatted(), "seen")?; + let sent_msg = smtp.send(account, &msg)?; + backend.add_msg(mbox, &sent_msg, "seen")?; printer.print_struct("Template successfully sent") } diff --git a/src/output/output_utils.rs b/src/output/output_utils.rs index 779089b..2053479 100644 --- a/src/output/output_utils.rs +++ b/src/output/output_utils.rs @@ -1,6 +1,9 @@ -use anyhow::Result; +use anyhow::{anyhow, Context, Result}; use log::debug; -use std::process::Command; +use std::{ + io::prelude::*, + process::{Command, Stdio}, +}; /// TODO: move this in a more approriate place. pub fn run_cmd(cmd: &str) -> Result { @@ -14,3 +17,25 @@ pub fn run_cmd(cmd: &str) -> Result { Ok(String::from_utf8(output.stdout)?) } + +pub fn pipe_cmd(cmd: &str, data: &[u8]) -> Result> { + let mut res = Vec::new(); + + let process = Command::new(cmd) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .with_context(|| format!("cannot spawn process from command {:?}", cmd))?; + process + .stdin + .ok_or_else(|| anyhow!("cannot get stdin"))? + .write_all(data) + .with_context(|| "cannot write data to stdin")?; + process + .stdout + .ok_or_else(|| anyhow!("cannot get stdout"))? + .read_to_end(&mut res) + .with_context(|| "cannot read data from stdout")?; + + Ok(res) +} diff --git a/src/smtp/smtp_service.rs b/src/smtp/smtp_service.rs index 9eeccc1..35d79bd 100644 --- a/src/smtp/smtp_service.rs +++ b/src/smtp/smtp_service.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use lettre::{ self, transport::smtp::{ @@ -7,13 +7,12 @@ use lettre::{ }, Transport, }; -use log::debug; +use std::convert::TryInto; -use crate::{config::AccountConfig, msg::Msg}; +use crate::{config::AccountConfig, msg::Msg, output::pipe_cmd}; pub trait SmtpService { - fn send_msg(&mut self, account: &AccountConfig, msg: &Msg) -> Result; - fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()>; + fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result>; } pub struct LettreService<'a> { @@ -21,7 +20,7 @@ pub struct LettreService<'a> { transport: Option, } -impl<'a> LettreService<'a> { +impl LettreService<'_> { fn transport(&mut self) -> Result<&SmtpTransport> { if let Some(ref transport) = self.transport { Ok(transport) @@ -55,24 +54,25 @@ impl<'a> LettreService<'a> { } } -impl<'a> SmtpService for LettreService<'a> { - fn send_msg(&mut self, account: &AccountConfig, msg: &Msg) -> Result { - debug!("sending message…"); - let sendable_msg = msg.into_sendable_msg(account)?; - self.transport()?.send(&sendable_msg)?; - Ok(sendable_msg) - } +impl SmtpService for LettreService<'_> { + fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result> { + let envelope: lettre::address::Envelope = msg.try_into()?; + let mut msg = msg.into_sendable_msg(account)?.formatted(); - fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()> { - debug!("sending raw message…"); - self.transport()?.send_raw(envelope, msg)?; - Ok(()) + if let Some(cmd) = account.hooks.pre_send.as_deref() { + for cmd in cmd.split('|') { + msg = pipe_cmd(cmd.trim(), &msg) + .with_context(|| format!("cannot execute pre-send hook {:?}", cmd))? + } + }; + + self.transport()?.send_raw(&envelope, &msg)?; + Ok(msg) } } impl<'a> From<&'a AccountConfig> for LettreService<'a> { fn from(account: &'a AccountConfig) -> Self { - debug!("init SMTP service"); Self { account, transport: None,