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:
Malte Meiboom
2026-07-09 12:49:47 +02:00
parent 94c94d1fce
commit ca58d73050
5 changed files with 395 additions and 56 deletions
+74 -4
View File
@@ -5,6 +5,11 @@
use std::ffi::CString;
use std::fmt;
use anyhow::Result;
use crate::types::recipient::Recipient;
#[derive(Debug, PartialEq, Clone)]
pub struct CHeader {
lines: Vec<(CString, CString)>
}
@@ -52,6 +57,47 @@ impl CHeader {
pub fn iter_lines(&self) -> std::slice::Iter<'_, (CString, CString)> {
self.lines.iter()
}
/// Remove a recipient from a value taken from a to, cc or bcc header
/// line. Only the email address is matched.
pub fn remove_recipient(header_value: &CString, to_remove: &Vec<Recipient>) -> Result<CString> {
let content = String::from_utf8_lossy(header_value.as_bytes());
let parts = content.split(",")
.filter(|part| {
let mut keep = true;
let p = part.to_ascii_lowercase();
for rcpt in to_remove {
let rcpt = rcpt.email.to_ascii_lowercase();
if p.contains(&rcpt) { keep = false; break; }
}
keep
})
.collect::<Vec<&str>>();
CString::new(parts.join(","))
.map_err(|e| e.into())
}
/// Returns a new header with `to_remove` removed from to, cc and bcc.
pub fn without_recipients(&self, to_remove: &Vec<Recipient>) -> CHeader {
let mut result = CHeader::new();
for (key, value) in self.iter_lines() {
let new_value = match key.as_bytes().to_ascii_lowercase().as_slice() {
b"to"
| b"cc"
| b"bcc" => Self::remove_recipient(value, to_remove),
_ => Ok(value.clone())
};
if let Ok(value) = new_value {
if ! value.is_empty() {
result.append(key.clone(), value);
}
}
}
result
}
}
impl fmt::Display for CHeader {
@@ -75,10 +121,12 @@ impl fmt::Display for CHeader {
#[cfg(test)]
pub mod tests {
use std::ffi::CString;
use crate::types::recipient::Recipient;
use super::CHeader;
#[test]
#[test]
pub fn basic_functionality() {
let cheader = CHeader::new()
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
@@ -87,7 +135,7 @@ pub mod tests {
assert_eq!("Content-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n", cheader.to_string());
}
#[test]
#[test]
pub fn find() {
let cheader = CHeader::new()
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
@@ -103,7 +151,7 @@ pub mod tests {
assert_eq!("text/plain", value);
}
#[test]
#[test]
pub fn find_none() {
let cheader = CHeader::new()
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
@@ -114,7 +162,7 @@ pub mod tests {
assert!(line.is_none());
}
#[test]
#[test]
pub fn find_with_default() {
let cheader = CHeader::new()
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
@@ -124,5 +172,27 @@ pub mod tests {
assert_eq!(value, "baaz");
}
#[test]
pub fn remove_recipients() {
let header = CHeader::new()
.add(CString::new("To").unwrap(), CString::new("Alice <alice@example.com>, Bob <bob@other.org>").unwrap())
.add(CString::new("Cc").unwrap(), CString::new("carol@example.com").unwrap());
let modified_header = header.without_recipients(
&vec![ Recipient::from("bob@other.org".to_string()) ]
);
let expected_header = CHeader::new()
.add(CString::new("To").unwrap(), CString::new("Alice <alice@example.com>").unwrap())
.add(CString::new("Cc").unwrap(), CString::new("carol@example.com").unwrap());
assert_eq!(modified_header, expected_header);
let modified_header = header.without_recipients(
&vec![ Recipient::from("CaRol@exAmple.Com".to_string()) ]
);
let expected_header = CHeader::new()
.add(CString::new("To").unwrap(), CString::new("Alice <alice@example.com>, Bob <bob@other.org>").unwrap());
assert_eq!(modified_header, expected_header);
}
}