Merge branch 'malte/mail_splitting' into 'main'
Mail splitting See merge request husk-project/husk-milter!9
This commit is contained in:
Generated
+404
-441
File diff suppressed because it is too large
Load Diff
+12
-3
@@ -14,17 +14,18 @@ rust-version = "1.85"
|
||||
aho-corasick = "1.1.4"
|
||||
anyhow = "1.0.100"
|
||||
bytes = "1.11.1"
|
||||
chrono = "0.4.45"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
fastrand = "2.4.1"
|
||||
indymilter = "0.3.0"
|
||||
lettre = "0.11.22"
|
||||
log = "0.4.30"
|
||||
log4rs = "1.3.0"
|
||||
sequoia-cert-store = "0.7.3"
|
||||
sequoia-directories = "0.1.0"
|
||||
#sequoia-keystore = "0.7.2"
|
||||
sequoia-keystore = { git = "https://gitlab.com/sequoia-pgp/sequoia-keystore", branch = "malte/softkey-refresh" }
|
||||
sequoia-keystore = "0.7.2"
|
||||
sequoia-net = "0.30.1"
|
||||
sequoia-openpgp = "2.3.0"
|
||||
sequoia-openpgp = "2.4.0"
|
||||
sequoia-wot = "0.15.0"
|
||||
serde = "1.0.228"
|
||||
serde_derive = "1.0.228"
|
||||
@@ -41,3 +42,11 @@ daemonix = "0.1.0"
|
||||
anyhow = "1.0.100"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
clap_complete = "4"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
[patch.crates-io]
|
||||
sequoia-cert-store = { git = "https://gitlab.com/sequoia-pgp/sequoia-cert-store", branch = "malte/assert_base_dir" }
|
||||
sequoia-keystore = { git = "https://gitlab.com/sequoia-pgp/sequoia-keystore", branch = "malte/softkey-refresh" }
|
||||
|
||||
+37
-7
@@ -12,13 +12,14 @@ delete_unix_user() { # name
|
||||
|
||||
# send a mail and wait for delivery
|
||||
send_test_mail() { # email_address
|
||||
echo "send test mail to $1" >&2
|
||||
USER=$(echo $1 | cut -f 1 -d '@')
|
||||
# create an empty mailbox file
|
||||
touch /var/mail/$USER
|
||||
chown $USER:mail /var/mail/$USER
|
||||
chmod 600 /var/mail/$USER
|
||||
|
||||
echo -e "Subject: testmail\n\ntest mail" | sendmail $1
|
||||
echo -e "To: $1\nSubject: testmail\n\ntest mail" | sendmail -t
|
||||
inotifywait -qq -t 3 -e close_write /var/mail/$USER
|
||||
}
|
||||
|
||||
@@ -27,10 +28,10 @@ test_header() { # text
|
||||
echo "##############################################"
|
||||
echo $1
|
||||
echo "Workdir: $WORKDIR"
|
||||
echo "##############################################"
|
||||
echo "----------------------------------------------"
|
||||
}
|
||||
|
||||
# Generate a key outside the cert store. The key will not be
|
||||
# Generate a key outside the key store. The key will not be
|
||||
# authenticated.
|
||||
gen_key() { # email
|
||||
FILE=$(echo $1 | cut -f 1 -d '@')
|
||||
@@ -41,31 +42,60 @@ gen_key() { # email
|
||||
echo $FPR
|
||||
}
|
||||
|
||||
expect_mail() { # unix_user mode
|
||||
# Generate a key outside the key store. The key will not be
|
||||
# authenticated. Import the certificate of the key.
|
||||
gen_cert() { # email
|
||||
FILE=$(echo $1 | cut -f 1 -d '@')
|
||||
FPR=$(sq key generate --home none --shared-key --without-password \
|
||||
--email $1 \
|
||||
--output $WORKDIR/${FILE}_sk.pgp \
|
||||
--rev-cert $WORKDIR/${FILE}.rev |& grep "Fingerprint:" | cut -d ':' -f 2)
|
||||
sq cert import $WORKDIR/${FILE}_sk.pgp > /dev/null 2>&1
|
||||
echo $FPR
|
||||
}
|
||||
|
||||
authenticate() { # Fingerprint
|
||||
sq pki link add --cert $1 --all > /dev/null 2>&1
|
||||
}
|
||||
|
||||
expect_mail() { # unix_user mode [signature_fpr]
|
||||
if [ -f /var/mail/$1 ] ; then
|
||||
case $2 in
|
||||
encrypted)
|
||||
if grep -q "BEGIN PGP MESSAGE" /var/mail/$1 ; then
|
||||
echo "GOOD: found expected encrypted mail" >&2
|
||||
R=0
|
||||
if [ "$3" != "" ] ; then
|
||||
FOUND_FPR=$(sq packet dump /var/mail/$1 | grep "Issuer Fingerprint:" | cut -d ':' -f 2 | tr -d ' ')
|
||||
|
||||
if [ "$3" != "$FOUND_FPR" ] ; then
|
||||
echo "BAD: Found wrong signature '$FOUND_FPR' - expected '$3'" >&2
|
||||
R=1
|
||||
else
|
||||
echo "GOOD: found right signature" >&2
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "expected encrypted mail for $1" >&2
|
||||
echo "BAD: encryped mail expected but not found" >&2
|
||||
R=1
|
||||
fi
|
||||
;;
|
||||
unencrypted)
|
||||
if grep -q "BEGIN PGP MESSAGE" /var/mail/$1 ; then
|
||||
echo "expected unencrypted mail for $1" >&2
|
||||
echo "BAD: expected unencrypted mail for $1" >&2
|
||||
R=1
|
||||
else
|
||||
echo "GOOD: found expected unencrypted mail" >&2
|
||||
R=0
|
||||
fi
|
||||
;;
|
||||
delivered)
|
||||
echo "GOOD: mail delivery worked" >&2
|
||||
R=0
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "no mail for $1" >&2
|
||||
echo "BAD: no mail for $1" >&2
|
||||
R=1
|
||||
fi
|
||||
echo $R
|
||||
|
||||
@@ -14,12 +14,11 @@ $HUSK_BIN introducer list --config config/config.toml
|
||||
create_unix_user fred
|
||||
FRED_FPR=$(sq key generate --shared-key --without-password --name "Fred" --email "fred@example.org" 2>&1 | grep "Fingerprint:" | cut -d ':' -f 2)
|
||||
sleep 1
|
||||
sq pki link retract --cert $FRED_FPR --all
|
||||
sq pki link retract --cert $FRED_FPR --all > /dev/null 2>&1
|
||||
|
||||
sq pki vouch add --certifier $ELLEN_FPR --cert $FRED_FPR --email "fred@example.org"
|
||||
sq pki vouch add --certifier $ELLEN_FPR --cert $FRED_FPR --email "fred@example.org" > /dev/null 2>&1
|
||||
|
||||
send_test_mail fred@example.org
|
||||
|
||||
RESULT_CODE=$(expect_mail fred encrypted)
|
||||
|
||||
if [ "$RESULT_CODE" == "1" ] ; then
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
test_header "Add locals"
|
||||
|
||||
sq key generate --shared-key --without-password --email 'hubert@example.com' --output /tmp/hubert.pgp --rev-cert /tmp/hubert_rev.pgp
|
||||
sq key generate --shared-key --without-password --email 'hubert@example.com' --output /tmp/hubert.pgp --rev-cert /tmp/hubert_rev.pgp > /dev/null 2>&1
|
||||
|
||||
cd $HUSK_DIR
|
||||
|
||||
@@ -16,47 +16,27 @@ if ! $HUSK_BIN locals list --config config/config.toml | grep -q "hubert@example
|
||||
fi
|
||||
|
||||
# send a mail, husk should encrypt it
|
||||
echo "send mail to hubert@example.com"
|
||||
create_unix_user hubert
|
||||
send_test_mail hubert@example.com
|
||||
|
||||
if [ -f /var/mail/hubert ] ; then
|
||||
if grep -q "BEGIN PGP MESSAGE" /var/mail/hubert ; then
|
||||
RESULT_CODE=0
|
||||
else
|
||||
echo "no encryption detected"
|
||||
delete_unix_user hubert
|
||||
exit 1
|
||||
fi
|
||||
# remove mail
|
||||
rm /var/mail/hubert
|
||||
else
|
||||
echo "no mail detected"
|
||||
R=$(expect_mail hubert encrypted)
|
||||
if [ $R != 0 ] ; then
|
||||
delete_unix_user hubert
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
# remove hubert as local
|
||||
|
||||
echo "removing hubert as local"
|
||||
HUBERT_FPR=$(sq cert list --cert-email hubert@example.com 2>/dev/null | head -n 1 | cut -d ' ' -f 3)
|
||||
$HUSK_BIN locals remove --cert $HUBERT_FPR --config config/config.toml
|
||||
|
||||
echo "send mail to hubert@example.com"
|
||||
rm /var/mail/hubert
|
||||
send_test_mail hubert@example.com
|
||||
|
||||
if [ -f /var/mail/hubert ] ; then
|
||||
if grep -q "BEGIN PGP MESSAGE" /var/mail/hubert ; then
|
||||
echo "encryption detected where there should be none"
|
||||
RESULT_CODE=1
|
||||
else
|
||||
RESULT_CODE=0
|
||||
fi
|
||||
else
|
||||
echo "no mail detected"
|
||||
RESULT_CODE=1
|
||||
fi
|
||||
R=$(expect_mail hubert unencrypted)
|
||||
|
||||
delete_unix_user hubert
|
||||
|
||||
exit $RESULT_CODE
|
||||
exit $R
|
||||
|
||||
|
||||
@@ -5,39 +5,28 @@
|
||||
|
||||
test_header "Signing"
|
||||
|
||||
# sq key generate --shared-key --without-password --email 'signing@example.com' --output /tmp/signing.pgp --rev-cert /tmp/signing.rev
|
||||
|
||||
SIGN_FPR=$(sq key generate --own-key --without-password --name "Signing Key" 2>&1 | grep "Fingerprint:" | cut -d ':' -f 2)
|
||||
SIGN_FPR_GEN=$(sq key generate --own-key --without-password --name "Signing Key" 2>&1 | grep "Fingerprint:" | cut -d ':' -f 2 | tr -d ' ')
|
||||
|
||||
cd $HUSK_DIR
|
||||
|
||||
$HUSK_BIN --config config/config.toml signkey set --cert $SIGN_FPR
|
||||
$HUSK_BIN --config config/config.toml signkey set --cert $SIGN_FPR_GEN
|
||||
SIGN_FPR=$($HUSK_BIN --config config/config.toml signkey show | cut -d ':' -f 2 | tr -d ' ')
|
||||
echo "found signing key $SIGN_FPR"
|
||||
|
||||
if [ "$SIGN_FPR" != "$SIGN_FPR_GEN" ] ; then
|
||||
echo "setting new siging key failed, found $SIGN_FPR expected $SIGN_FPR_GEN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
create_unix_user juliette
|
||||
JULIETTE_FPR=$(sq key generate --own-key --without-password --name "Juliette" --email "juliette@example.com" 2>&1 | grep "Fingerprint:" | cut -d ':' -f 2)
|
||||
sq pki vouch add --certifier-userid "CA" --cert $JULIETTE_FPR --all > /dev/null 2>&1
|
||||
# sq pki vouch add --certifier-userid "CA" --cert $JULIETTE_FPR --all > /dev/null 2>&1
|
||||
|
||||
# send a mail, husk should encrypt it
|
||||
echo "send mail to juliette@example.com"
|
||||
send_test_mail juliette@example.com
|
||||
|
||||
if [ -f /var/mail/juliette ] ; then
|
||||
if grep -q "BEGIN PGP MESSAGE" /var/mail/juliette ; then
|
||||
|
||||
FOUND_FPR=$(sq packet dump /var/mail/juliette | grep "Issuer Fingerprint:" | cut -d ':' -f 2 | tr -d ' ')
|
||||
|
||||
if [ "$SIGN_FPR" != "$FOUND_FPR" ] ; then
|
||||
echo "Found wrong fingerprint '$FOUND_FPR' - expected '$SIGN_FPR'"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "no encryption detected"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "no mail detected"
|
||||
R=$(expect_mail juliette encrypted $SIGN_FPR)
|
||||
if [ $R != 0 ] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -50,23 +39,6 @@ echo "new signing key $SIGN_FPR"
|
||||
# remove old mail
|
||||
rm /var/mail/juliette
|
||||
|
||||
echo "send mail to juliette@example.com"
|
||||
send_test_mail juliette@example.com
|
||||
|
||||
if [ -f /var/mail/juliette ] ; then
|
||||
if grep -q "BEGIN PGP MESSAGE" /var/mail/juliette ; then
|
||||
|
||||
FOUND_FPR=$(sq packet dump /var/mail/juliette | grep "Issuer Fingerprint:" | cut -d ':' -f 2 | tr -d ' ')
|
||||
|
||||
if [ "$SIGN_FPR" != "$FOUND_FPR" ] ; then
|
||||
echo "Found wrong fingerprint '$FOUND_FPR' - expected '$SIGN_FPR'"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "no encryption detected"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "no mail detected"
|
||||
exit 1
|
||||
fi
|
||||
exit $(expect_mail juliette encrypted $SIGN_FPR)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
# source some functions
|
||||
. common.sh
|
||||
|
||||
test_header "Mail split"
|
||||
|
||||
create_unix_user karl
|
||||
KARL_FPR=$(gen_cert karl@example.com)
|
||||
authenticate $KARL_FPR
|
||||
|
||||
create_unix_user laura
|
||||
|
||||
echo -e "To: karl@example.com, Laura <laura@example.com>\nSubject: Testmail\n\nTestmail." | sendmail -t
|
||||
sleep 1
|
||||
|
||||
R=1
|
||||
if [ $(expect_mail karl encrypted) == 0 ] ; then
|
||||
if [ $(expect_mail laura unencrypted) == 0 ] ; then
|
||||
R=0
|
||||
fi
|
||||
fi
|
||||
|
||||
exit $R
|
||||
+9
-30
@@ -23,7 +23,6 @@ use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
|
||||
use sequoia_openpgp::policy::StandardPolicy;
|
||||
use sequoia_openpgp::serialize::stream::{Message, Armorer, Encryptor, Signer, LiteralWriter};
|
||||
use sequoia_openpgp::{Fingerprint, Cert, Packet};
|
||||
use sequoia_openpgp::cert::raw::RawCertParser;
|
||||
use sequoia_openpgp::types::SignatureType;
|
||||
use sequoia_cert_store::{Store, CertStore, LazyCert, StoreUpdate};
|
||||
use sequoia_cert_store::store::MergePublicCollectStats;
|
||||
@@ -45,35 +44,15 @@ const HUSK_SIG_NOTATION: &str = "signing_key@husk-project.org";
|
||||
/// Returns the local trust root from the cert_store. If the trust root
|
||||
/// cannot be found, it returns an error.
|
||||
pub fn get_local_trust_root(cert_store: &CertStore) -> anyhow::Result<Cert> {
|
||||
|
||||
let root = cert_store.certd()
|
||||
.and_then(|certd| {
|
||||
match certd.certd().get(sequoia_cert_store::store::openpgp_cert_d::TRUST_ROOT) {
|
||||
Ok(Some((_tag, bytes))) => Some(bytes),
|
||||
Ok(None) => None,
|
||||
Err(_) => None
|
||||
}
|
||||
})
|
||||
.and_then(|bytes| {
|
||||
match RawCertParser::from_bytes(&bytes[..]) {
|
||||
Ok(mut parser) => {
|
||||
match parser.next() {
|
||||
Some(Ok(cert)) => {
|
||||
Cert::from_bytes(cert.as_bytes()).ok()
|
||||
},
|
||||
Some(Err(_))
|
||||
| None => None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
}
|
||||
});
|
||||
|
||||
match root {
|
||||
Some(r) => Ok(r),
|
||||
None => Err(HuskError::NoLocalTrustRoot.into())
|
||||
if let Some((root, created)) = cert_store.certd().and_then(|certd| {
|
||||
certd.trust_root().ok() }) {
|
||||
if created {
|
||||
log::info!("Local trust root not found - created one");
|
||||
}
|
||||
Ok(root.to_cert()?.clone())
|
||||
} else {
|
||||
Err(HuskError::NoLocalTrustRoot.into())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Returns all local certificates with an authenticated binding to user id 'email'.
|
||||
@@ -255,7 +234,7 @@ pub async fn get_certificates<'hc>(context: &HuskContext<'hc>, email: &str)
|
||||
|
||||
for introducer in &context.introducers {
|
||||
log::debug!("consider introducer {:?}",
|
||||
introducer.cert.userids().map(|u| u.userid()).collect::<Vec<_>>());
|
||||
introducer.cert.self_signed_userids().map(|u| u.userid()).collect::<Vec<_>>());
|
||||
if introducer.can_introduce(format!("<{}>", email).as_str()) {
|
||||
log::debug!(" introducer regex matches!");
|
||||
match lookup_certificates(context, email).await {
|
||||
|
||||
+158
-60
@@ -1,7 +1,7 @@
|
||||
//! the actual daemon
|
||||
|
||||
use anyhow;
|
||||
use bytes::Bytes;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use std::ffi::CString;
|
||||
use tokio::signal;
|
||||
use tokio::net::TcpListener;
|
||||
@@ -18,6 +18,7 @@ use indymilter::{
|
||||
ContextActions,
|
||||
};
|
||||
|
||||
use crate::types::defaults::HELO_REINJECT_ID;
|
||||
use crate::types::husk_context::HuskContext;
|
||||
use crate::config::{HuskConfig, HuskConfigContainer};
|
||||
use crate::mail;
|
||||
@@ -39,8 +40,8 @@ impl Daemon {
|
||||
let callbacks = Callbacks::new()
|
||||
.on_negotiate(|cx, actions, opts| Box::pin(Self::handle_negotiate(cx, actions, opts)))
|
||||
.on_connect(|cx, hostname, socket_info| Box::pin(Self::handle_connect(cx, hostname, socket_info)))
|
||||
.on_helo(move |cx, hostname| Box::pin(Self::handle_helo(cx, config_container.clone(), hostname)))
|
||||
.on_mail(|cx, args| Box::pin(Self::handle_mail(cx, args)))
|
||||
.on_helo(|cx, hostname| Box::pin(Self::handle_helo(cx, hostname)))
|
||||
.on_mail(move |cx, args| Box::pin(Self::handle_mail(cx, config_container.clone(), args)))
|
||||
.on_rcpt(|cx, args| Box::pin(Self::handle_rcpt(cx, args)))
|
||||
.on_data(|cx| Box::pin(Self::handle_data(cx)))
|
||||
.on_header(|cx, name, value| Box::pin(Self::handle_header(cx, name, value)))
|
||||
@@ -80,26 +81,19 @@ impl Daemon {
|
||||
Status::Continue
|
||||
}
|
||||
|
||||
async fn handle_helo(cx: &mut Context<HuskContext<'_>>, config_container: HuskConfigContainer, hostname: CString) -> Status {
|
||||
async fn handle_helo(_cx: &mut Context<HuskContext<'_>>, hostname: CString) -> Status {
|
||||
log::debug!("HELO: hostname: {hostname:?}");
|
||||
|
||||
let config: HuskConfig = config_container.into();
|
||||
|
||||
// setup Context
|
||||
match HuskContext::new(&config) {
|
||||
Ok(context) => {
|
||||
log::debug!("found introducers: {:?}", context.introducers);
|
||||
cx.data = Some(context);
|
||||
Status::Continue
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
Status::Tempfail
|
||||
}
|
||||
if hostname.to_str() == Ok(HELO_REINJECT_ID) {
|
||||
// Reinjected mail detected - this mail is unencrypted because
|
||||
// all recipients lack a valid certificate. Accept mail.
|
||||
Status::Accept
|
||||
} else {
|
||||
Status::Continue
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_mail(cx: &mut Context<HuskContext<'_>>, args: Vec<CString>) -> Status {
|
||||
async fn handle_mail(cx: &mut Context<HuskContext<'_>>, config_container: HuskConfigContainer, args: Vec<CString>) -> Status {
|
||||
log::debug!("MAIL: {args:?}");
|
||||
|
||||
let mut sender: Option<String> = None;
|
||||
@@ -117,7 +111,20 @@ impl Daemon {
|
||||
context.set_sender(sender.unwrap());
|
||||
Status::Continue
|
||||
} else {
|
||||
Status::Tempfail
|
||||
let config: HuskConfig = config_container.into();
|
||||
|
||||
// setup Context
|
||||
match HuskContext::new(&config) {
|
||||
Ok(mut context) => {
|
||||
context.set_sender(sender.unwrap());
|
||||
cx.data = Some(context);
|
||||
Status::Continue
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
Status::Tempfail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,58 +209,149 @@ impl Daemon {
|
||||
return Status::Accept;
|
||||
}
|
||||
|
||||
// possible protection
|
||||
// none -> accept
|
||||
// partial -> accept (later: split)
|
||||
// full -> encrypt
|
||||
let from = match context.mail.sender.clone() {
|
||||
Some(f) => f,
|
||||
None => {
|
||||
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 mut body: BytesMut = body.into();
|
||||
body.extend_from_slice(mail::generate_footer(&vec![]).as_bytes());
|
||||
let inner_mail = MailContext::generate_inner_mail(&context.mail.header, body.into());
|
||||
|
||||
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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
// 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 mut unencryted_body: BytesMut = body.clone().into();
|
||||
unencryted_body.extend_from_slice(mail::generate_footer(&encryptable).as_bytes());
|
||||
let header = context.mail.header.without_recipients(&encryptable);
|
||||
if mail::smtp_inject(
|
||||
from,
|
||||
&unencryptable,
|
||||
header,
|
||||
&unencryted_body.into()).await.is_err() {
|
||||
// smtp_inject() already logged the error.
|
||||
return Status::Tempfail;
|
||||
}
|
||||
|
||||
// Encrypt for remaining recipients.
|
||||
let mut encrypted_body: BytesMut = body.into();
|
||||
encrypted_body.extend_from_slice(mail::generate_footer(&unencryptable).as_bytes());
|
||||
let header = context.mail.header.without_recipients(&unencryptable);
|
||||
let inner_mail = MailContext::generate_inner_mail(&header, encrypted_body.into());
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
+150
@@ -1,8 +1,24 @@
|
||||
//! 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) -> Option<UserID> {
|
||||
match rcpt.to_str() {
|
||||
Ok(s) => Some(UserID::from(s)),
|
||||
@@ -10,6 +26,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 +49,136 @@ 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(defaults::HELO_REINJECT_ID.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())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
+74
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@ pub const SUBJECT_REPLACEMENT: &str = "...";
|
||||
|
||||
pub const PID_FILE_LOCATION: &str = "/run/husk/pid";
|
||||
|
||||
pub const FOOTER: &str = "---\nProcessed by Husk at {date}.\n";
|
||||
pub const FOOTER_RECIPIENTS: &str = "This mail was also send to {rcpts}.\n";
|
||||
|
||||
/// This ID is used when reinjecting an unencrypted mail into the MTA.
|
||||
pub const HELO_REINJECT_ID: &str = "husk.reinject";
|
||||
|
||||
/// Default logging configuration.
|
||||
pub const CONSOLE_LOGGING: &str = "
|
||||
[appenders.console]
|
||||
|
||||
@@ -24,4 +24,6 @@ pub enum HuskError {
|
||||
InvalidDomainName(String),
|
||||
#[error("Certificate has no user ids - {0}")]
|
||||
NoUserIDs(String),
|
||||
#[error("Failing to send mail: {0}")]
|
||||
SmtpInjectionFailure(String),
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ use std::fmt::{Display, Formatter};
|
||||
impl Display for Introducer {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
// XXX: This is unsafe as ANSI control chars are not handled.
|
||||
let uids = self.cert.userids()
|
||||
let uids = self.cert.self_signed_userids()
|
||||
.map(|u| String::from_utf8_lossy(u.userid().value()).to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
+97
-11
@@ -15,17 +15,15 @@ use crate::types::mime::header::Header;
|
||||
use crate::types::mime::part::Part;
|
||||
use crate::types::mime::multi_part::MultiPart;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum ProtectionPossibility {
|
||||
pub enum ProtectionPossibility<'mc> {
|
||||
// no recipient has a cert
|
||||
NoProtection,
|
||||
// some recipients have a cert (but not all)
|
||||
Partial,
|
||||
Partial(Vec<Recipient<'mc>>, Vec<Recipient<'mc>>),
|
||||
// all recipients have a cert
|
||||
Full
|
||||
}
|
||||
|
||||
|
||||
pub struct MailContext<'mc> {
|
||||
pub sender: Option<String>,
|
||||
pub recipients: Vec<Recipient<'mc>>,
|
||||
@@ -84,8 +82,8 @@ impl<'mc> MailContext<'mc> {
|
||||
buffer.into()
|
||||
}
|
||||
|
||||
pub fn generate_inner_mail(&self, body: Bytes) -> MultiPart<'mc> {
|
||||
let header = &self.header;
|
||||
pub fn generate_inner_mail(header: &CHeader, body: Bytes) -> MultiPart<'mc> {
|
||||
//let header = &self.header;
|
||||
let inner = Part::with_body(body)
|
||||
.set_header(Header::new()
|
||||
.add(
|
||||
@@ -158,22 +156,27 @@ impl<'mc> Default for MailContext<'mc> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&MailContext<'_>> for ProtectionPossibility {
|
||||
fn from(mail_context: &MailContext<'_>) -> ProtectionPossibility {
|
||||
impl<'mc> From<&MailContext<'mc>> for ProtectionPossibility<'mc> {
|
||||
fn from(mail_context: &MailContext<'mc>) -> ProtectionPossibility<'mc> {
|
||||
let mut can_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() {
|
||||
if rcpt.can_encrypt() {
|
||||
can_encrypt = true;
|
||||
encrypted_rcpt.push(rcpt.clone());
|
||||
} else {
|
||||
cannot_encrypt = true;
|
||||
unencrypted_rcpt.push(rcpt.clone());
|
||||
}
|
||||
}
|
||||
|
||||
match (can_encrypt, cannot_encrypt) {
|
||||
(true, false) => ProtectionPossibility::Full,
|
||||
(true, true) => ProtectionPossibility::Partial,
|
||||
(true, true) => ProtectionPossibility::Partial(encrypted_rcpt, unencrypted_rcpt),
|
||||
(false, true) => ProtectionPossibility::NoProtection,
|
||||
// (false, false) occurs if there are no recipients
|
||||
(false, false) => ProtectionPossibility::NoProtection,
|
||||
@@ -185,8 +188,17 @@ impl From<&MailContext<'_>> for ProtectionPossibility {
|
||||
mod tests {
|
||||
|
||||
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::mail_context::MailContext;
|
||||
use crate::types::recipient::Recipient;
|
||||
|
||||
use super::ProtectionPossibility;
|
||||
|
||||
#[test]
|
||||
fn test_inner_mail() {
|
||||
@@ -202,8 +214,7 @@ mod tests {
|
||||
|
||||
let orig_body = "This is the body".as_bytes().to_vec();
|
||||
|
||||
let context = MailContext::from_header(orig_header);
|
||||
let inner_mail = context.generate_inner_mail(orig_body.into());
|
||||
let inner_mail = MailContext::generate_inner_mail(&orig_header, orig_body.into());
|
||||
|
||||
let intended_result = [
|
||||
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());
|
||||
}
|
||||
|
||||
#[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