Add footer
- Add a footer to each mail resulting from a split into an encrypted and an unencrypted version. The footer contains the recipients of the mail which are excluded in the corresponding stream. - Move the construction of the `HuskContext` to the `mail` milter-command, as `helo` is used by scrapers to detect mailservers. - Use `CertD::trust_root()` to get the trust root - it creates one if no root can be found. - Add `chrono` dependency.
This commit is contained in:
Generated
+3
-3
@@ -1261,6 +1261,7 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"daemonix",
|
||||
@@ -2610,8 +2611,7 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
[[package]]
|
||||
name = "sequoia-cert-store"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4e07e420427eb5e0dc3bb61172d181fa3c87afb2a77e8c629e920f54a291a6d"
|
||||
source = "git+https://gitlab.com/sequoia-pgp/sequoia-cert-store?branch=malte%2Fassert_base_dir#ef9928239012a78b16be82b70420973a06e309a4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossbeam",
|
||||
@@ -2624,7 +2624,7 @@ dependencies = [
|
||||
"sequoia-net",
|
||||
"sequoia-openpgp",
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -14,6 +14,7 @@ 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"
|
||||
@@ -46,3 +47,6 @@ 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" }
|
||||
|
||||
+8
-29
@@ -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'.
|
||||
|
||||
+30
-22
@@ -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;
|
||||
@@ -39,8 +39,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 +80,15 @@ 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();
|
||||
// XXX: check for reinjection
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
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 +106,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +215,9 @@ impl Daemon {
|
||||
match ProtectionPossibility::from(&context.mail) {
|
||||
ProtectionPossibility::Full => {
|
||||
log::debug!("completely encryptable");
|
||||
let inner_mail = MailContext::generate_inner_mail(&context.mail.header, body);
|
||||
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 an error occures the body is not exchanged
|
||||
match crypto::encrypt(context, inner_mail.to_string().as_bytes(), &context.mail.recipients) {
|
||||
@@ -257,19 +261,23 @@ impl Daemon {
|
||||
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,
|
||||
&body).await.is_err() {
|
||||
&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, body);
|
||||
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) {
|
||||
|
||||
+54
@@ -11,8 +11,10 @@ use lettre::transport::smtp::{
|
||||
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;
|
||||
|
||||
@@ -128,3 +130,55 @@ pub async fn smtp_inject(from: String, to: &Vec<Recipient<'_>>, header: CHeader,
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ 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";
|
||||
|
||||
/// Default logging configuration.
|
||||
pub const CONSOLE_LOGGING: &str = "
|
||||
[appenders.console]
|
||||
|
||||
Reference in New Issue
Block a user