mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-22 02:50:19 +00:00
add pre-send hook (#178)
This commit is contained in:
parent
212f5e6eb1
commit
f79e0ae4fb
10 changed files with 94 additions and 34 deletions
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
7
src/config/hooks.rs
Normal 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>,
|
||||
}
|
|
@ -126,6 +126,9 @@ pub mod config {
|
|||
|
||||
pub mod format;
|
||||
pub use format::*;
|
||||
|
||||
pub mod hooks;
|
||||
pub use hooks::*;
|
||||
}
|
||||
|
||||
pub mod compl;
|
||||
|
|
|
@ -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),
|
||||
}?;
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue