add pre-send hook (#178)

This commit is contained in:
Clément DOUIN 2022-03-05 00:42:11 +01:00
parent 212f5e6eb1
commit f79e0ae4fb
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
10 changed files with 94 additions and 34 deletions

View file

@ -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

View file

@ -36,6 +36,9 @@ pub struct AccountConfig {
/// Represents mailbox aliases.
pub mailboxes: HashMap<String, String>,
/// 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<MailAddr> {
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 {

View file

@ -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<String, String>,
/// Represents hooks.
pub hooks: Option<Hooks>,
$(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(),
}
}
}

7
src/config/hooks.rs Normal file
View file

@ -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<String>,
}

View file

@ -126,6 +126,9 @@ pub mod config {
pub mod format;
pub use format::*;
pub mod hooks;
pub use hooks::*;
}
pub mod compl;

View file

@ -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<lettre::address::Envelope> for Msg {
type Error = Error;
fn try_into(self) -> Result<lettre::address::Envelope> {
let from = match self.from.and_then(|addrs| addrs.extract_single_info()) {
(&self).try_into()
}
}
impl TryInto<lettre::address::Envelope> for &Msg {
type Error = Error;
fn try_into(self) -> Result<lettre::address::Envelope> {
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),
}?;

View file

@ -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(())
}

View file

@ -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")
}

View file

@ -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<String> {
@ -14,3 +17,25 @@ pub fn run_cmd(cmd: &str) -> Result<String> {
Ok(String::from_utf8(output.stdout)?)
}
pub fn pipe_cmd(cmd: &str, data: &[u8]) -> Result<Vec<u8>> {
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)
}

View file

@ -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<lettre::Message>;
fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()>;
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>>;
}
pub struct LettreService<'a> {
@ -21,7 +20,7 @@ pub struct LettreService<'a> {
transport: Option<SmtpTransport>,
}
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<lettre::Message> {
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<Vec<u8>> {
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,