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:
+126
-41
@@ -202,58 +202,143 @@ impl Daemon {
|
|||||||
return Status::Accept;
|
return Status::Accept;
|
||||||
}
|
}
|
||||||
|
|
||||||
// possible protection
|
let from = match context.mail.sender.clone() {
|
||||||
// none -> accept
|
Some(f) => f,
|
||||||
// partial -> accept (later: split)
|
None => {
|
||||||
// full -> encrypt
|
log::error!("No sender given");
|
||||||
|
return Status::Tempfail;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let protection = ProtectionPossibility::from(&context.mail);
|
match ProtectionPossibility::from(&context.mail) {
|
||||||
|
ProtectionPossibility::Full => {
|
||||||
|
log::debug!("completely encryptable");
|
||||||
|
let inner_mail = MailContext::generate_inner_mail(&context.mail.header, body);
|
||||||
|
|
||||||
if protection == ProtectionPossibility::Full {
|
// if an error occures the body is not exchanged
|
||||||
|
match crypto::encrypt(context, inner_mail.to_string().as_bytes(), &context.mail.recipients) {
|
||||||
|
Ok(encrypted) => {
|
||||||
|
// replace the body of the mail with an encrypted,
|
||||||
|
// mime encoded version.
|
||||||
|
let outer_mail = MailContext::generate_outer_mail(encrypted);
|
||||||
|
if cx.actions.replace_body(
|
||||||
|
outer_mail
|
||||||
|
.body_to_string()
|
||||||
|
.as_bytes()).await.is_err() {
|
||||||
|
log::error!("Cannot exchange body");
|
||||||
|
return Status::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
let inner_mail = context.mail.generate_inner_mail(body);
|
// adjust headers
|
||||||
|
// This is mostly Content-Type related.
|
||||||
// if an error occures the body is not exchanged
|
for line in outer_mail.get_header().iter_lines() {
|
||||||
match crypto::encrypt(context, inner_mail.to_string().as_bytes(), &context.mail.recipients) {
|
// XXX: handle errors
|
||||||
Ok(encrypted) => {
|
if let Ok(key) = line.key_as_cstring() {
|
||||||
// replace the body of the mail with an encrypted,
|
if let Ok(value) = line.value_as_cstring() {
|
||||||
// mime encoded version.
|
let _ = cx.actions.change_header(
|
||||||
let outer_mail = MailContext::generate_outer_mail(encrypted);
|
key, 1,
|
||||||
if cx.actions.replace_body(
|
Some(value)
|
||||||
outer_mail
|
).await;
|
||||||
.body_to_string()
|
}
|
||||||
.as_bytes()).await.is_err() {
|
|
||||||
log::error!("Cannot exchange body");
|
|
||||||
return Status::Continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// adjust headers
|
|
||||||
// This is mostly Content-Type related.
|
|
||||||
for line in outer_mail.get_header().iter_lines() {
|
|
||||||
// XXX: handle errors
|
|
||||||
if let Ok(key) = line.key_as_cstring() {
|
|
||||||
if let Ok(value) = line.value_as_cstring() {
|
|
||||||
let _ = cx.actions.change_header(
|
|
||||||
key, 1,
|
|
||||||
Some(value)
|
|
||||||
).await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Change the subject
|
||||||
|
let _ = cx.actions.change_header(
|
||||||
|
"Subject",
|
||||||
|
1,
|
||||||
|
Some(context.subject_replacement.clone())).await;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{}", e);
|
||||||
}
|
}
|
||||||
// Change the subject
|
|
||||||
let _ = cx.actions.change_header(
|
|
||||||
"Subject",
|
|
||||||
1,
|
|
||||||
Some(context.subject_replacement.clone())).await;
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("{}", e);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
ProtectionPossibility::Partial(encryptable, unencryptable) => {
|
||||||
|
log::debug!("partially encryptable");
|
||||||
|
// Generate a new mail for the unencryptable recipients.
|
||||||
|
|
||||||
|
let header = context.mail.header.without_recipients(&encryptable);
|
||||||
|
if mail::smtp_inject(
|
||||||
|
from,
|
||||||
|
&unencryptable,
|
||||||
|
header,
|
||||||
|
&body).await.is_err() {
|
||||||
|
// smtp_inject() already logged the error.
|
||||||
|
return Status::Tempfail;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt for remaining recipients.
|
||||||
|
let header = context.mail.header.without_recipients(&unencryptable);
|
||||||
|
let inner_mail = MailContext::generate_inner_mail(&header, body);
|
||||||
|
|
||||||
|
// If an error occures, the body is not exchanged.
|
||||||
|
match crypto::encrypt(context, inner_mail.to_string().as_bytes(), &encryptable) {
|
||||||
|
Ok(encrypted) => {
|
||||||
|
// replace the body of the mail with an encrypted,
|
||||||
|
// mime encoded version.
|
||||||
|
let outer_mail = MailContext::generate_outer_mail(encrypted);
|
||||||
|
if cx.actions.replace_body(
|
||||||
|
outer_mail
|
||||||
|
.body_to_string()
|
||||||
|
.as_bytes()).await.is_err() {
|
||||||
|
log::error!("Cannot exchange body");
|
||||||
|
return Status::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust headers
|
||||||
|
// XXX error handling
|
||||||
|
|
||||||
|
// Remove unencryptable recipients from 'to', 'cc'
|
||||||
|
// and 'bcc'.
|
||||||
|
for (key, value) in header.iter_lines() {
|
||||||
|
match key.as_bytes().to_ascii_lowercase().as_slice() {
|
||||||
|
b"to"
|
||||||
|
| b"cc"
|
||||||
|
| b"bcc" => {
|
||||||
|
let _ = cx.actions.change_header(
|
||||||
|
key.clone(),
|
||||||
|
1,
|
||||||
|
Some(value.clone())
|
||||||
|
).await;
|
||||||
|
},
|
||||||
|
_ => { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also remove recipients via the milter protocol.
|
||||||
|
for rcpt in unencryptable {
|
||||||
|
let _ = cx.actions.delete_recipient(rcpt.email).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is Content-Type related.
|
||||||
|
for line in outer_mail.get_header().iter_lines() {
|
||||||
|
// XXX: handle errors
|
||||||
|
if let Ok(key) = line.key_as_cstring() {
|
||||||
|
if let Ok(value) = line.value_as_cstring() {
|
||||||
|
let _ = cx.actions.change_header(
|
||||||
|
key, 1,
|
||||||
|
Some(value)
|
||||||
|
).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Change the subject.
|
||||||
|
let _ = cx.actions.change_header(
|
||||||
|
"Subject",
|
||||||
|
1,
|
||||||
|
Some(context.subject_replacement.clone())).await;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ProtectionPossibility::NoProtection => {
|
||||||
|
log::debug!("not encryptable");
|
||||||
|
// do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::logger().flush();
|
|
||||||
Status::Continue
|
Status::Continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+96
@@ -1,8 +1,22 @@
|
|||||||
//! mail functions
|
//! mail functions
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use bytes::BytesMut;
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use sequoia_openpgp::packet::UserID;
|
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> {
|
pub fn to_userid(rcpt: &CString) -> Option<UserID> {
|
||||||
match rcpt.to_str() {
|
match rcpt.to_str() {
|
||||||
Ok(s) => Some(UserID::from(s)),
|
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> {
|
pub fn to_email(rcpt: &CString) -> Option<String> {
|
||||||
if let Some(u) = to_userid(rcpt) {
|
if let Some(u) = to_userid(rcpt) {
|
||||||
if let Ok(Some(e)) = u.email() { Some(e.to_string()) }
|
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 {
|
pub fn is_domain(domain: &str) -> bool {
|
||||||
is_email(format!("test@{}", domain).as_str())
|
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())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+74
-4
@@ -5,6 +5,11 @@
|
|||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::types::recipient::Recipient;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct CHeader {
|
pub struct CHeader {
|
||||||
lines: Vec<(CString, CString)>
|
lines: Vec<(CString, CString)>
|
||||||
}
|
}
|
||||||
@@ -52,6 +57,47 @@ impl CHeader {
|
|||||||
pub fn iter_lines(&self) -> std::slice::Iter<'_, (CString, CString)> {
|
pub fn iter_lines(&self) -> std::slice::Iter<'_, (CString, CString)> {
|
||||||
self.lines.iter()
|
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 {
|
impl fmt::Display for CHeader {
|
||||||
@@ -75,10 +121,12 @@ impl fmt::Display for CHeader {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
use crate::types::recipient::Recipient;
|
||||||
|
|
||||||
use super::CHeader;
|
use super::CHeader;
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn basic_functionality() {
|
pub fn basic_functionality() {
|
||||||
let cheader = CHeader::new()
|
let cheader = CHeader::new()
|
||||||
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
|
.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());
|
assert_eq!("Content-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n", cheader.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn find() {
|
pub fn find() {
|
||||||
let cheader = CHeader::new()
|
let cheader = CHeader::new()
|
||||||
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
|
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
|
||||||
@@ -103,7 +151,7 @@ pub mod tests {
|
|||||||
assert_eq!("text/plain", value);
|
assert_eq!("text/plain", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn find_none() {
|
pub fn find_none() {
|
||||||
let cheader = CHeader::new()
|
let cheader = CHeader::new()
|
||||||
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
|
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
|
||||||
@@ -114,7 +162,7 @@ pub mod tests {
|
|||||||
assert!(line.is_none());
|
assert!(line.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn find_with_default() {
|
pub fn find_with_default() {
|
||||||
let cheader = CHeader::new()
|
let cheader = CHeader::new()
|
||||||
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
|
.add(CString::new("Content-Type").unwrap(), CString::new("text/plain").unwrap())
|
||||||
@@ -124,5 +172,27 @@ pub mod tests {
|
|||||||
|
|
||||||
assert_eq!(value, "baaz");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,4 +24,6 @@ pub enum HuskError {
|
|||||||
InvalidDomainName(String),
|
InvalidDomainName(String),
|
||||||
#[error("Certificate has no user ids - {0}")]
|
#[error("Certificate has no user ids - {0}")]
|
||||||
NoUserIDs(String),
|
NoUserIDs(String),
|
||||||
|
#[error("Failing to send mail: {0}")]
|
||||||
|
SmtpInjectionFailure(String),
|
||||||
}
|
}
|
||||||
|
|||||||
+97
-11
@@ -15,17 +15,15 @@ use crate::types::mime::header::Header;
|
|||||||
use crate::types::mime::part::Part;
|
use crate::types::mime::part::Part;
|
||||||
use crate::types::mime::multi_part::MultiPart;
|
use crate::types::mime::multi_part::MultiPart;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
pub enum ProtectionPossibility<'mc> {
|
||||||
pub enum ProtectionPossibility {
|
|
||||||
// no recipient has a cert
|
// no recipient has a cert
|
||||||
NoProtection,
|
NoProtection,
|
||||||
// some recipients have a cert (but not all)
|
// some recipients have a cert (but not all)
|
||||||
Partial,
|
Partial(Vec<Recipient<'mc>>, Vec<Recipient<'mc>>),
|
||||||
// all recipients have a cert
|
// all recipients have a cert
|
||||||
Full
|
Full
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub struct MailContext<'mc> {
|
pub struct MailContext<'mc> {
|
||||||
pub sender: Option<String>,
|
pub sender: Option<String>,
|
||||||
pub recipients: Vec<Recipient<'mc>>,
|
pub recipients: Vec<Recipient<'mc>>,
|
||||||
@@ -84,8 +82,8 @@ impl<'mc> MailContext<'mc> {
|
|||||||
buffer.into()
|
buffer.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_inner_mail(&self, body: Bytes) -> MultiPart<'mc> {
|
pub fn generate_inner_mail(header: &CHeader, body: Bytes) -> MultiPart<'mc> {
|
||||||
let header = &self.header;
|
//let header = &self.header;
|
||||||
let inner = Part::with_body(body)
|
let inner = Part::with_body(body)
|
||||||
.set_header(Header::new()
|
.set_header(Header::new()
|
||||||
.add(
|
.add(
|
||||||
@@ -158,22 +156,27 @@ impl<'mc> Default for MailContext<'mc> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&MailContext<'_>> for ProtectionPossibility {
|
impl<'mc> From<&MailContext<'mc>> for ProtectionPossibility<'mc> {
|
||||||
fn from(mail_context: &MailContext<'_>) -> ProtectionPossibility {
|
fn from(mail_context: &MailContext<'mc>) -> ProtectionPossibility<'mc> {
|
||||||
let mut can_encrypt = false;
|
let mut can_encrypt = false;
|
||||||
let mut cannot_encrypt = false;
|
let mut cannot_encrypt = false;
|
||||||
|
|
||||||
|
let mut encrypted_rcpt = Vec::new();
|
||||||
|
let mut unencrypted_rcpt = Vec::new();
|
||||||
|
|
||||||
for rcpt in mail_context.recipients.iter() {
|
for rcpt in mail_context.recipients.iter() {
|
||||||
if rcpt.can_encrypt() {
|
if rcpt.can_encrypt() {
|
||||||
can_encrypt = true;
|
can_encrypt = true;
|
||||||
|
encrypted_rcpt.push(rcpt.clone());
|
||||||
} else {
|
} else {
|
||||||
cannot_encrypt = true;
|
cannot_encrypt = true;
|
||||||
|
unencrypted_rcpt.push(rcpt.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match (can_encrypt, cannot_encrypt) {
|
match (can_encrypt, cannot_encrypt) {
|
||||||
(true, false) => ProtectionPossibility::Full,
|
(true, false) => ProtectionPossibility::Full,
|
||||||
(true, true) => ProtectionPossibility::Partial,
|
(true, true) => ProtectionPossibility::Partial(encrypted_rcpt, unencrypted_rcpt),
|
||||||
(false, true) => ProtectionPossibility::NoProtection,
|
(false, true) => ProtectionPossibility::NoProtection,
|
||||||
// (false, false) occurs if there are no recipients
|
// (false, false) occurs if there are no recipients
|
||||||
(false, false) => ProtectionPossibility::NoProtection,
|
(false, false) => ProtectionPossibility::NoProtection,
|
||||||
@@ -185,8 +188,17 @@ impl From<&MailContext<'_>> for ProtectionPossibility {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use sequoia_cert_store::LazyCert;
|
||||||
|
use sequoia_openpgp::cert::CertBuilder;
|
||||||
|
|
||||||
use crate::types::cheader::CHeader;
|
use crate::types::cheader::CHeader;
|
||||||
use crate::types::mail_context::MailContext;
|
use crate::types::mail_context::MailContext;
|
||||||
|
use crate::types::recipient::Recipient;
|
||||||
|
|
||||||
|
use super::ProtectionPossibility;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_inner_mail() {
|
fn test_inner_mail() {
|
||||||
@@ -202,8 +214,7 @@ mod tests {
|
|||||||
|
|
||||||
let orig_body = "This is the body".as_bytes().to_vec();
|
let orig_body = "This is the body".as_bytes().to_vec();
|
||||||
|
|
||||||
let context = MailContext::from_header(orig_header);
|
let inner_mail = MailContext::generate_inner_mail(&orig_header, orig_body.into());
|
||||||
let inner_mail = context.generate_inner_mail(orig_body.into());
|
|
||||||
|
|
||||||
let intended_result = [
|
let intended_result = [
|
||||||
format!("Content-Type: multipart/mixed; boundary=\"{}\"; protected-headers=\"v1\"", inner_mail.get_boundary()),
|
format!("Content-Type: multipart/mixed; boundary=\"{}\"; protected-headers=\"v1\"", inner_mail.get_boundary()),
|
||||||
@@ -220,5 +231,80 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(intended_result, inner_mail.to_string());
|
assert_eq!(intended_result, inner_mail.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_recipient_splitting() -> Result<()> {
|
||||||
|
let (alice_cert, _) = CertBuilder::general_purpose(["alice@example.com"])
|
||||||
|
.generate()?;
|
||||||
|
|
||||||
|
// Mixed recipients: alice can encrypt, bob doesn't.
|
||||||
|
let mut mail_context = MailContext::new();
|
||||||
|
mail_context.recipients = vec![
|
||||||
|
Recipient {
|
||||||
|
email: "alice@example.com".to_string(),
|
||||||
|
certs: vec![Arc::new(LazyCert::from_cert(alice_cert))],
|
||||||
|
},
|
||||||
|
Recipient {
|
||||||
|
email: "bob@example.com".to_string(),
|
||||||
|
certs: Vec::new()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let protection = ProtectionPossibility::from(&mail_context);
|
||||||
|
|
||||||
|
assert!(matches!(protection, ProtectionPossibility::Partial(_, _)));
|
||||||
|
|
||||||
|
if let ProtectionPossibility::Partial(a, b) = protection {
|
||||||
|
assert_eq!(a.len(), 1);
|
||||||
|
assert_eq!(b.len(), 1);
|
||||||
|
|
||||||
|
let alice = a.iter().next().unwrap();
|
||||||
|
assert_eq!(alice.email, "alice@example.com");
|
||||||
|
|
||||||
|
let bob = b.iter().next().unwrap();
|
||||||
|
assert_eq!(bob.email, "bob@example.com");
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("this should be unreachable"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both recipients can encrypt.
|
||||||
|
let (alice_cert, _) = CertBuilder::general_purpose(["alice@example.com"])
|
||||||
|
.generate()?;
|
||||||
|
let (bob_cert, _) = CertBuilder::general_purpose(["bob@example.com"])
|
||||||
|
.generate()?;
|
||||||
|
let mut mail_context = MailContext::new();
|
||||||
|
mail_context.recipients = vec![
|
||||||
|
Recipient {
|
||||||
|
email: "alice@example.com".to_string(),
|
||||||
|
certs: vec![Arc::new(LazyCert::from_cert(alice_cert))],
|
||||||
|
},
|
||||||
|
Recipient {
|
||||||
|
email: "bob@example.com".to_string(),
|
||||||
|
certs: vec![Arc::new(LazyCert::from_cert(bob_cert))],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let protection = ProtectionPossibility::from(&mail_context);
|
||||||
|
|
||||||
|
assert!(matches!(protection, ProtectionPossibility::Full));
|
||||||
|
|
||||||
|
// No recipient can encrypt.
|
||||||
|
let mut mail_context = MailContext::new();
|
||||||
|
mail_context.recipients = vec![
|
||||||
|
Recipient {
|
||||||
|
email: "alice@example.com".to_string(),
|
||||||
|
certs: Vec::new()
|
||||||
|
},
|
||||||
|
Recipient {
|
||||||
|
email: "bob@example.com".to_string(),
|
||||||
|
certs: Vec::new()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let protection = ProtectionPossibility::from(&mail_context);
|
||||||
|
|
||||||
|
assert!(matches!(protection, ProtectionPossibility::NoProtection));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user