- Reject user ids containing ANSI control characters when creating a new signing key. - Prevent adding certificates twice when querying online sources if they contain multiple encryption keys. - Dedup introducers if there are multiple authenticating paths. - When listing locals, skip the signing key. - Check if a mail can be encrypted at all due to available certificates. If not tell the MTA that the processing is done. - Make `Introducer` comparable (`PartialEq`, `Eq` and `Hash`). - minor polishing
187 lines
5.4 KiB
Rust
187 lines
5.4 KiB
Rust
//! 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 chrono::Utc;
|
|
|
|
use crate::types::cheader::CHeader;
|
|
use crate::types::defaults;
|
|
use crate::types::errors::HuskError;
|
|
use crate::types::recipient::Recipient;
|
|
|
|
/// Parse the user id fields from a string.
|
|
pub fn to_userid(rcpt: &CString) -> Result<UserID> {
|
|
let rcpt = rcpt.to_str()?;
|
|
Ok(UserID::from(rcpt))
|
|
}
|
|
|
|
/// Get the email from the string representation of a user id.
|
|
pub fn to_email(rcpt: &CString) -> Result<String> {
|
|
let u = to_userid(rcpt)?;
|
|
|
|
if let Some(e) = u.email()? {
|
|
Ok(e.to_string())
|
|
} else {
|
|
Err(HuskError::CannotConvertToEmail(u.to_string()).into())
|
|
}
|
|
}
|
|
|
|
/// Checks if the parameter has a valid email format.
|
|
pub fn is_email(email: &str) -> bool {
|
|
let uid = UserID::from(email);
|
|
|
|
if let Ok(Some(e)) = uid.email() {
|
|
e == email
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Checks if the parameter has a valid domain format.
|
|
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)?;
|
|
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()?);
|
|
}
|
|
|
|
// XXX: add secret token?
|
|
let my_id = ClientId::Domain(defaults::HELO_REINJECT_ID.to_owned());
|
|
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())
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn generate_footer(rcpts: &Vec<Recipient>) -> String {
|
|
internal_generate_footer(Utc::now().to_rfc2822(), rcpts)
|
|
}
|
|
|
|
pub(crate) fn internal_generate_footer(now: String, other_rpcts: &Vec<Recipient>) -> String {
|
|
let mut footer = defaults::FOOTER.replace("{date}", now.as_str());
|
|
|
|
if ! other_rpcts.is_empty() {
|
|
let rcpts = other_rpcts.iter()
|
|
.map(|r| r.email.clone())
|
|
.collect::<Vec<String>>()
|
|
.join(", ");
|
|
footer.push_str(
|
|
defaults::FOOTER_RECIPIENTS.replace(
|
|
"{rcpts}",
|
|
&rcpts).as_str()
|
|
);
|
|
}
|
|
footer
|
|
}
|
|
|
|
|
|
#[cfg(test)]
|
|
pub mod tests {
|
|
|
|
use crate::mail::internal_generate_footer;
|
|
use crate::types::recipient::Recipient;
|
|
|
|
#[test]
|
|
fn test_footer() {
|
|
|
|
let rcpts = vec![
|
|
Recipient {
|
|
email: "alice@example.com".to_string(),
|
|
certs: Vec::new()
|
|
},
|
|
Recipient {
|
|
email: "bob@example.com".to_string(),
|
|
certs: Vec::new()
|
|
}
|
|
];
|
|
|
|
let footer = internal_generate_footer(
|
|
"1.1.1970".to_string(),
|
|
&rcpts
|
|
);
|
|
|
|
assert!(footer.contains("1.1.1970"));
|
|
assert!(footer.contains("alice@example.com"));
|
|
assert!(footer.contains("bob@example.com"));
|
|
}
|
|
}
|