Add mail splitting
- If a mail has more then one recipient and not for all recipients an authenticated certificate can be obtained, the mail processing is split into 2 streams - one that encrypts and an other which sends the mail in plaintext. - For both streams the header and the list of recipients is adjusted accordingly. - As the milter interface only allows one stream, the second stream is reinjected into the MTA (via SMTP). The MTA is hardcoded to be localhost:25.
This commit is contained in:
+96
@@ -1,8 +1,22 @@
|
||||
//! mail functions
|
||||
|
||||
use anyhow::Result;
|
||||
use bytes::Bytes;
|
||||
use bytes::BytesMut;
|
||||
use std::ffi::CString;
|
||||
use sequoia_openpgp::packet::UserID;
|
||||
use lettre::transport::smtp::{
|
||||
client::SmtpConnection, commands::*, extension::ClientId, SMTP_PORT,
|
||||
};
|
||||
use lettre::Address;
|
||||
use tokio::task::JoinSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::types::cheader::CHeader;
|
||||
use crate::types::errors::HuskError;
|
||||
use crate::types::recipient::Recipient;
|
||||
|
||||
/// Parse the user id fields from a string.
|
||||
pub fn to_userid(rcpt: &CString) -> Option<UserID> {
|
||||
match rcpt.to_str() {
|
||||
Ok(s) => Some(UserID::from(s)),
|
||||
@@ -10,6 +24,7 @@ pub fn to_userid(rcpt: &CString) -> Option<UserID> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the email from the string representation of a user id.
|
||||
pub fn to_email(rcpt: &CString) -> Option<String> {
|
||||
if let Some(u) = to_userid(rcpt) {
|
||||
if let Ok(Some(e)) = u.email() { Some(e.to_string()) }
|
||||
@@ -32,3 +47,84 @@ pub fn is_email(email: &str) -> bool {
|
||||
pub fn is_domain(domain: &str) -> bool {
|
||||
is_email(format!("test@{}", domain).as_str())
|
||||
}
|
||||
|
||||
pub async fn smtp_inject(from: String, to: &Vec<Recipient<'_>>, header: CHeader, body: &Bytes) -> Result<()> {
|
||||
|
||||
let mut joinset: JoinSet<Result<()>> = JoinSet::new();
|
||||
|
||||
log::debug!("smtp inject: from {}", from);
|
||||
|
||||
// build message
|
||||
let mut header_size = 0;
|
||||
for (key, value) in header.iter_lines() {
|
||||
header_size += key.count_bytes();
|
||||
header_size += value.count_bytes();
|
||||
header_size += 4; // 4 is ': ' plus '\r\n'
|
||||
}
|
||||
|
||||
// The size of the whole message the sum of header and body size + '\r\n'
|
||||
// as the dividing empty line.
|
||||
let mut message = BytesMut::with_capacity(header_size + body.len() + 2);
|
||||
for (key, value) in header.iter_lines() {
|
||||
message.extend_from_slice(key.as_bytes());
|
||||
message.extend_from_slice(": ".as_bytes());
|
||||
message.extend_from_slice(value.as_bytes());
|
||||
message.extend_from_slice("\r\n".as_bytes());
|
||||
}
|
||||
message.extend_from_slice(b"\r\n");
|
||||
message.extend_from_slice(&body[..]);
|
||||
|
||||
// convert addresses
|
||||
let from_email = CString::new(from.clone())?;
|
||||
let from_email = to_email(&from_email).unwrap(); // XXX
|
||||
let from_email: Address = from_email.try_into()?;
|
||||
|
||||
let mut rcpts: Vec<Address> = Vec::new();
|
||||
for rcpt in to {
|
||||
let rcpt = rcpt.email.clone();
|
||||
rcpts.push(rcpt.try_into()?);
|
||||
}
|
||||
|
||||
let my_id = ClientId::Domain("husk.reinject".to_owned()); // TODO: add secret token?
|
||||
joinset.spawn(async move {
|
||||
|
||||
// XXX
|
||||
// We connect to localhost - every other host would require implementing
|
||||
// TLS. The milter protocol is unencrypted anyways.
|
||||
match SmtpConnection::connect(("localhost", SMTP_PORT), Some(Duration::from_secs(5)), &my_id, None, None) {
|
||||
Ok(mut client) => {
|
||||
log::debug!("connected - start sending");
|
||||
|
||||
client.command(Mail::new(Some(from_email),vec![]))?;
|
||||
for rcpt in rcpts {
|
||||
client.command(Rcpt::new(rcpt, vec![]))?;
|
||||
}
|
||||
client.command(Data)?;
|
||||
client.message(&message)?;
|
||||
client.command(Quit)?;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("cannot connect to localhost:{} - {:?}", SMTP_PORT, e);
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
match joinset.join_all().await.first() {
|
||||
Some(Ok(_)) => {
|
||||
log::debug!("mail injection done.");
|
||||
Ok(())
|
||||
},
|
||||
Some(Err(e)) => {
|
||||
log::error!("mail injection failed: {:?}", e);
|
||||
Err(HuskError::SmtpInjectionFailure(e.to_string()).into())
|
||||
},
|
||||
_ => {
|
||||
log::error!("mail injection failed without reason");
|
||||
Err(HuskError::SmtpInjectionFailure("no reason".to_string()).into())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user