mirror of
https://github.com/soywod/himalaya.git
synced 2024-09-28 20:21:13 +00:00
add pre-send hook (#178)
This commit is contained in:
parent
212f5e6eb1
commit
f79e0ae4fb
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- SMTP pre-send hook [#178]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Improve `attachments` command [#281]
|
- 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
|
[#162]: https://github.com/soywod/himalaya/issues/162
|
||||||
[#176]: https://github.com/soywod/himalaya/issues/176
|
[#176]: https://github.com/soywod/himalaya/issues/176
|
||||||
[#172]: https://github.com/soywod/himalaya/issues/172
|
[#172]: https://github.com/soywod/himalaya/issues/172
|
||||||
|
[#178]: https://github.com/soywod/himalaya/issues/178
|
||||||
[#181]: https://github.com/soywod/himalaya/issues/181
|
[#181]: https://github.com/soywod/himalaya/issues/181
|
||||||
[#185]: https://github.com/soywod/himalaya/issues/185
|
[#185]: https://github.com/soywod/himalaya/issues/185
|
||||||
[#186]: https://github.com/soywod/himalaya/issues/186
|
[#186]: https://github.com/soywod/himalaya/issues/186
|
||||||
|
|
|
@ -36,6 +36,9 @@ pub struct AccountConfig {
|
||||||
/// Represents mailbox aliases.
|
/// Represents mailbox aliases.
|
||||||
pub mailboxes: HashMap<String, String>,
|
pub mailboxes: HashMap<String, String>,
|
||||||
|
|
||||||
|
/// Represents hooks.
|
||||||
|
pub hooks: Hooks,
|
||||||
|
|
||||||
/// Represents the SMTP host.
|
/// Represents the SMTP host.
|
||||||
pub smtp_host: String,
|
pub smtp_host: String,
|
||||||
/// Represents the SMTP port.
|
/// Represents the SMTP port.
|
||||||
|
@ -155,6 +158,7 @@ impl<'a> AccountConfig {
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
format: base_account.format.unwrap_or_default(),
|
format: base_account.format.unwrap_or_default(),
|
||||||
mailboxes: base_account.mailboxes.clone(),
|
mailboxes: base_account.mailboxes.clone(),
|
||||||
|
hooks: base_account.hooks.unwrap_or_default(),
|
||||||
default: base_account.default.unwrap_or_default(),
|
default: base_account.default.unwrap_or_default(),
|
||||||
email: base_account.email.to_owned(),
|
email: base_account.email.to_owned(),
|
||||||
|
|
||||||
|
@ -203,8 +207,7 @@ impl<'a> AccountConfig {
|
||||||
|
|
||||||
/// Builds the full RFC822 compliant address of the user account.
|
/// Builds the full RFC822 compliant address of the user account.
|
||||||
pub fn address(&self) -> Result<MailAddr> {
|
pub fn address(&self) -> Result<MailAddr> {
|
||||||
let has_special_chars =
|
let has_special_chars = "()<>[]:;@.,".contains(|c| self.display_name.contains(c));
|
||||||
"()<>[]:;@.,".contains(|special_char| self.display_name.contains(special_char));
|
|
||||||
let addr = if self.display_name.is_empty() {
|
let addr = if self.display_name.is_empty() {
|
||||||
self.email.clone()
|
self.email.clone()
|
||||||
} else if has_special_chars {
|
} else if has_special_chars {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
use crate::config::Format;
|
use crate::config::{Format, Hooks};
|
||||||
|
|
||||||
pub trait ToDeserializedBaseAccountConfig {
|
pub trait ToDeserializedBaseAccountConfig {
|
||||||
fn to_base(&self) -> DeserializedBaseAccountConfig;
|
fn to_base(&self) -> DeserializedBaseAccountConfig;
|
||||||
|
@ -84,6 +84,9 @@ macro_rules! make_account_config {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mailboxes: HashMap<String, String>,
|
pub mailboxes: HashMap<String, String>,
|
||||||
|
|
||||||
|
/// Represents hooks.
|
||||||
|
pub hooks: Option<Hooks>,
|
||||||
|
|
||||||
$(pub $element: $ty),*
|
$(pub $element: $ty),*
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +117,7 @@ macro_rules! make_account_config {
|
||||||
pgp_decrypt_cmd: self.pgp_decrypt_cmd.clone(),
|
pgp_decrypt_cmd: self.pgp_decrypt_cmd.clone(),
|
||||||
|
|
||||||
mailboxes: self.mailboxes.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 mod format;
|
||||||
pub use format::*;
|
pub use format::*;
|
||||||
|
|
||||||
|
pub mod hooks;
|
||||||
|
pub use hooks::*;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod compl;
|
pub mod compl;
|
||||||
|
|
|
@ -24,7 +24,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Representation of a message.
|
/// Representation of a message.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct Msg {
|
pub struct Msg {
|
||||||
/// The sequence number of the message.
|
/// The sequence number of the message.
|
||||||
///
|
///
|
||||||
|
@ -359,15 +359,18 @@ impl Msg {
|
||||||
loop {
|
loop {
|
||||||
match choice::post_edit() {
|
match choice::post_edit() {
|
||||||
Ok(PostEditChoice::Send) => {
|
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
|
let sent_folder = account
|
||||||
.mailboxes
|
.mailboxes
|
||||||
.get("sent")
|
.get("sent")
|
||||||
.map(|s| s.as_str())
|
.map(|s| s.as_str())
|
||||||
.unwrap_or(DEFAULT_SENT_FOLDER);
|
.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()?;
|
msg_utils::remove_local_draft()?;
|
||||||
printer.print_struct("Message successfully sent")?;
|
printer.print_struct("Done!")?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Ok(PostEditChoice::Edit) => {
|
Ok(PostEditChoice::Edit) => {
|
||||||
|
@ -711,7 +714,19 @@ impl TryInto<lettre::address::Envelope> for Msg {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_into(self) -> Result<lettre::address::Envelope> {
|
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),
|
Some(addr) => addr.addr.parse().map(Some),
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}?;
|
}?;
|
||||||
|
|
|
@ -8,7 +8,6 @@ use log::{debug, info, trace};
|
||||||
use mailparse::addrparse;
|
use mailparse::addrparse;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
convert::TryInto,
|
|
||||||
fs,
|
fs,
|
||||||
io::{self, BufRead},
|
io::{self, BufRead},
|
||||||
};
|
};
|
||||||
|
@ -356,9 +355,8 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||||
.join("\r\n")
|
.join("\r\n")
|
||||||
};
|
};
|
||||||
trace!("raw message: {:?}", raw_msg);
|
trace!("raw message: {:?}", raw_msg);
|
||||||
let envelope: lettre::address::Envelope = Msg::from_tpl(&raw_msg)?.try_into()?;
|
let msg = Msg::from_tpl(&raw_msg)?;
|
||||||
trace!("envelope: {:?}", envelope);
|
smtp.send(&config, &msg)?;
|
||||||
smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?;
|
|
||||||
backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?;
|
backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||||
.join("\n")
|
.join("\n")
|
||||||
};
|
};
|
||||||
let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||||
let sent_msg = smtp.send_msg(account, &msg)?;
|
let sent_msg = smtp.send(account, &msg)?;
|
||||||
backend.add_msg(mbox, &sent_msg.formatted(), "seen")?;
|
backend.add_msg(mbox, &sent_msg, "seen")?;
|
||||||
printer.print_struct("Template successfully sent")
|
printer.print_struct("Template successfully sent")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Context, Result};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use std::process::Command;
|
use std::{
|
||||||
|
io::prelude::*,
|
||||||
|
process::{Command, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
/// TODO: move this in a more approriate place.
|
/// TODO: move this in a more approriate place.
|
||||||
pub fn run_cmd(cmd: &str) -> Result<String> {
|
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)?)
|
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::{
|
use lettre::{
|
||||||
self,
|
self,
|
||||||
transport::smtp::{
|
transport::smtp::{
|
||||||
|
@ -7,13 +7,12 @@ use lettre::{
|
||||||
},
|
},
|
||||||
Transport,
|
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 {
|
pub trait SmtpService {
|
||||||
fn send_msg(&mut self, account: &AccountConfig, msg: &Msg) -> Result<lettre::Message>;
|
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>>;
|
||||||
fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LettreService<'a> {
|
pub struct LettreService<'a> {
|
||||||
|
@ -21,7 +20,7 @@ pub struct LettreService<'a> {
|
||||||
transport: Option<SmtpTransport>,
|
transport: Option<SmtpTransport>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> LettreService<'a> {
|
impl LettreService<'_> {
|
||||||
fn transport(&mut self) -> Result<&SmtpTransport> {
|
fn transport(&mut self) -> Result<&SmtpTransport> {
|
||||||
if let Some(ref transport) = self.transport {
|
if let Some(ref transport) = self.transport {
|
||||||
Ok(transport)
|
Ok(transport)
|
||||||
|
@ -55,24 +54,25 @@ impl<'a> LettreService<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SmtpService for LettreService<'a> {
|
impl SmtpService for LettreService<'_> {
|
||||||
fn send_msg(&mut self, account: &AccountConfig, msg: &Msg) -> Result<lettre::Message> {
|
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>> {
|
||||||
debug!("sending message…");
|
let envelope: lettre::address::Envelope = msg.try_into()?;
|
||||||
let sendable_msg = msg.into_sendable_msg(account)?;
|
let mut msg = msg.into_sendable_msg(account)?.formatted();
|
||||||
self.transport()?.send(&sendable_msg)?;
|
|
||||||
Ok(sendable_msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()> {
|
if let Some(cmd) = account.hooks.pre_send.as_deref() {
|
||||||
debug!("sending raw message…");
|
for cmd in cmd.split('|') {
|
||||||
self.transport()?.send_raw(envelope, msg)?;
|
msg = pipe_cmd(cmd.trim(), &msg)
|
||||||
Ok(())
|
.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> {
|
impl<'a> From<&'a AccountConfig> for LettreService<'a> {
|
||||||
fn from(account: &'a AccountConfig) -> Self {
|
fn from(account: &'a AccountConfig) -> Self {
|
||||||
debug!("init SMTP service");
|
|
||||||
Self {
|
Self {
|
||||||
account,
|
account,
|
||||||
transport: None,
|
transport: None,
|
||||||
|
|
Loading…
Reference in a new issue