- Add functionality to query online sources (Keyservers, etc) for certificates. - Use introducers to determine which UserIDs should be fetched from online soources. Check certifications before importing certifiactes into the local cert store. - start docker image for local end-to-end testing.
257 lines
7.6 KiB
Rust
257 lines
7.6 KiB
Rust
//
|
|
// Husk milter
|
|
//
|
|
// the actual daemon
|
|
//
|
|
|
|
use anyhow;
|
|
use bytes::Bytes;
|
|
use std::ffi::CString;
|
|
use tokio::signal;
|
|
use tokio::net::TcpListener;
|
|
|
|
use indymilter::{
|
|
Actions,
|
|
Callbacks,
|
|
Context,
|
|
EomContext,
|
|
Macros,
|
|
NegotiateContext,
|
|
ProtoOpts,
|
|
SocketInfo,
|
|
Status,
|
|
Config,
|
|
ContextActions,
|
|
};
|
|
|
|
use crate::types::husk_context::HuskContext;
|
|
use crate::config::{HuskConfig, HuskConfigContainer};
|
|
use crate::mail;
|
|
use crate::crypto;
|
|
use crate::types::mail_context::ProtectionPossibility;
|
|
|
|
pub struct Daemon { }
|
|
|
|
impl Daemon {
|
|
|
|
pub async fn run(config_container: HuskConfigContainer) -> anyhow::Result<()> {
|
|
|
|
let config: HuskConfig = config_container.clone().into();
|
|
|
|
let listener = TcpListener::bind(&config.connection)
|
|
.await
|
|
.expect(" opening milter socket");
|
|
|
|
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_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)))
|
|
.on_eoh(|cx| Box::pin(Self::handle_eoh(cx)))
|
|
.on_body(|cx, chunk| Box::pin(Self::handle_body(cx, chunk)))
|
|
.on_eom(|cx| Box::pin(Self::handle_eom(cx)))
|
|
.on_abort(|cx| Box::pin(Self::handle_abort(cx)))
|
|
.on_close(|cx| Box::pin(Self::handle_close(cx)))
|
|
.on_unknown(|cx, arg| Box::pin(Self::handle_unknown(cx, arg)));
|
|
|
|
let config = Default::default();
|
|
|
|
indymilter::run(listener, callbacks, config, signal::ctrl_c())
|
|
.await
|
|
.expect("milter execution failed");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn handle_negotiate(
|
|
_cx: &mut NegotiateContext<HuskContext<'_>>,
|
|
actions: Actions,
|
|
opts: ProtoOpts,
|
|
) -> Status {
|
|
log::debug!("NEGOTIATE: actions: {actions:?} - opts: {opts:?}");
|
|
|
|
Status::AllOpts
|
|
}
|
|
|
|
async fn handle_connect(
|
|
_cx: &mut Context<HuskContext<'_>>,
|
|
hostname: CString,
|
|
socket_info: SocketInfo,
|
|
) -> Status {
|
|
log::debug!("CONNECT: hostname: {hostname:?} - socket_info: {socket_info:?}");
|
|
//Self::print_macros(&cx.macros);
|
|
|
|
Status::Continue
|
|
}
|
|
|
|
async fn handle_helo(cx: &mut Context<HuskContext<'_>>, config_container: HuskConfigContainer, 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).take();
|
|
Status::Continue
|
|
},
|
|
Err(e) => {
|
|
log::error!("{}", e);
|
|
Status::Tempfail
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn handle_mail(cx: &mut Context<HuskContext<'_>>, args: Vec<CString>) -> Status {
|
|
log::debug!("MAIL: {args:?}");
|
|
|
|
let mut sender: Option<String> = None;
|
|
|
|
for arg in args {
|
|
if let Some(s) = mail::to_email(&arg) {
|
|
sender = Some(s);
|
|
break;
|
|
}
|
|
}
|
|
if sender.is_none() { return Status::Tempfail; }
|
|
|
|
if let Some(ref mut context) = cx.data {
|
|
context.reset_mail();
|
|
context.set_sender(sender.unwrap());
|
|
Status::Continue
|
|
} else {
|
|
Status::Tempfail
|
|
}
|
|
}
|
|
|
|
async fn handle_rcpt(cx: &mut Context<HuskContext<'_>>, args: Vec<CString>) -> Status {
|
|
log::debug!("RCPT: {args:?}");
|
|
|
|
if let Some(ref mut context) = cx.data {
|
|
|
|
let mut rcpt: Option<String> = None;
|
|
for arg in args {
|
|
if let Some(s) = mail::to_email(&arg) {
|
|
rcpt = Some(s);
|
|
break;
|
|
}
|
|
}
|
|
if rcpt.is_none() {
|
|
Status::Tempfail
|
|
} else {
|
|
context.add_recipient(rcpt.unwrap()).await;
|
|
Status::Continue
|
|
}
|
|
|
|
} else {
|
|
Status::Tempfail
|
|
}
|
|
}
|
|
|
|
async fn handle_data(cx: &mut Context<HuskContext<'_>>) -> Status {
|
|
log::debug!("DATA");
|
|
|
|
if let Some(ref mut _context) = cx.data {
|
|
// XXX: check for early accepting
|
|
// if there are no recipients with authenticated certificates
|
|
// tell the MTA that this milter is done and the mail can be further
|
|
// processed.
|
|
Status::Continue
|
|
} else {
|
|
Status::Tempfail
|
|
}
|
|
}
|
|
|
|
async fn handle_header(_cx: &mut Context<HuskContext<'_>>, name: CString, value: CString) -> Status {
|
|
log::debug!("HEADER: {name:?} = {value:?}");
|
|
|
|
Status::Continue
|
|
}
|
|
|
|
async fn handle_eoh(_cx: &mut Context<HuskContext<'_>>) -> Status {
|
|
log::debug!("EOH");
|
|
|
|
Status::Continue
|
|
}
|
|
|
|
async fn handle_body(cx: &mut Context<HuskContext<'_>>, chunk: Bytes) -> Status {
|
|
log::debug!("BODY: chunk with {:?} bytes received", &chunk.len());
|
|
|
|
if let Some(ref mut context) = cx.data {
|
|
context.mail.add_chunk(chunk);
|
|
Status::Continue
|
|
} else {
|
|
Status::Tempfail
|
|
}
|
|
}
|
|
|
|
async fn handle_eom(cx: &mut EomContext<HuskContext<'_>>) -> Status {
|
|
log::debug!("EOM");
|
|
|
|
if let Some(ref mut context) = cx.data {
|
|
log::debug!("Mail from {:?} complete", context.mail.sender);
|
|
log::debug!("recipients: {:?}", context.mail.recipients);
|
|
log::debug!("body size: {}", context.mail.body_size);
|
|
|
|
let body = context.mail.collect_body();
|
|
|
|
// check if mail is already encrypted. If it is, keep it as such.
|
|
if crypto::is_encrypted(&body) {
|
|
log::info!("encrypted mail detected");
|
|
return Status::Accept;
|
|
}
|
|
|
|
|
|
// possible protection
|
|
// none -> accept
|
|
// partial -> accept (later: split)
|
|
// full -> encrypt
|
|
|
|
let protection = ProtectionPossibility::from(&context.mail);
|
|
|
|
if protection == ProtectionPossibility::Full {
|
|
// if an error occures the body is not exchanged
|
|
match crypto::encrypt(&context, &body, &context.mail.recipients) {
|
|
Ok(encrypted) => {
|
|
if cx.actions.replace_body(&encrypted).await.is_err() {
|
|
log::error!("Cannot exchange body");
|
|
}
|
|
},
|
|
Err(e) => {
|
|
log::error!("{}", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Status::Continue
|
|
}
|
|
|
|
async fn handle_abort(_cx: &mut Context<HuskContext<'_>>) -> Status {
|
|
log::debug!("ABORT");
|
|
|
|
Status::Continue
|
|
}
|
|
|
|
async fn handle_close(_cx: &mut Context<HuskContext<'_>>) -> Status {
|
|
log::debug!("CLOSE");
|
|
|
|
Status::Continue
|
|
}
|
|
|
|
async fn handle_unknown(_cx: &mut Context<HuskContext<'_>>, arg: CString) -> Status {
|
|
log::debug!("UNKNOWN: {arg:?}");
|
|
|
|
Status::Continue
|
|
}
|
|
|
|
fn print_macros(macros: &Macros) {
|
|
println!(" macros: {:?}", macros.to_hash_map());
|
|
}
|
|
|
|
}
|