use crate::{
auth::AuthSession, config::SessionConfig, header::auth_results::AuthenticationResultsHeader,
};
use byte_strings::c_str;
use indymilter::{
Actions, Callbacks, Context, EomContext, MacroStage, Macros, NegotiateContext, SocketInfo,
Status,
};
use log::{debug, error};
use std::{
borrow::Cow,
ffi::{CStr, CString},
sync::{Arc, RwLock},
};
trait SessionMut {
fn session(&mut self) -> &mut AuthSession;
}
impl SessionMut for Option<AuthSession> {
fn session(&mut self) -> &mut AuthSession {
self.as_mut().expect("milter context data not available")
}
}
trait MacrosExt {
fn get_string(&self, name: &CStr) -> Option<Cow<'_, str>>;
fn queue_id(&self) -> Cow<'_, str>;
}
impl MacrosExt for Macros {
fn get_string(&self, name: &CStr) -> Option<Cow<'_, str>> {
self.get(name).map(|v| v.to_string_lossy())
}
fn queue_id(&self) -> Cow<'_, str> {
self.get_string(c_str!("i"))
.unwrap_or_else(|| "NONE".into())
}
}
pub fn make_callbacks(session_config: Arc<RwLock<Arc<SessionConfig>>>) -> Callbacks<AuthSession> {
Callbacks::new()
.on_negotiate(move |cx, _, _| Box::pin(handle_negotiate(session_config.clone(), cx)))
.on_connect(|cx, _, socket_info| Box::pin(handle_connect(cx, socket_info)))
.on_helo(|cx, helo_host| Box::pin(handle_helo(cx, helo_host)))
.on_mail(|cx, smtp_args| Box::pin(handle_mail(cx, smtp_args)))
.on_header(|cx, name, value| Box::pin(handle_header(cx, name, value)))
.on_eom(|cx| Box::pin(handle_eom(cx)))
.on_abort(|cx| Box::pin(handle_abort(cx)))
.on_close(|cx| Box::pin(handle_close(cx)))
}
async fn handle_negotiate(
session_config: Arc<RwLock<Arc<SessionConfig>>>,
context: &mut NegotiateContext<AuthSession>,
) -> Status {
let session_config = session_config
.read()
.expect("could not get configuration read lock")
.clone();
let config = &session_config.config;
if !config.dry_run() {
context.requested_actions |= Actions::ADD_HEADER;
if config.delete_incoming_authentication_results() {
context.requested_actions |= Actions::CHANGE_HEADER;
}
}
let macros = &mut context.requested_macros;
if config.hostname().is_none() {
macros.insert(MacroStage::Connect, c_str!("j").into());
}
if config.trust_authenticated_senders() {
macros.insert(MacroStage::Mail, c_str!("{auth_authen}").into());
}
macros.insert(MacroStage::Data, c_str!("i").into());
context.data = Some(AuthSession::new(session_config));
Status::Continue
}
async fn handle_connect(context: &mut Context<AuthSession>, socket_info: SocketInfo) -> Status {
let ip = match socket_info {
SocketInfo::Inet(addr) => addr.ip(),
_ => {
debug!("accepted connection with no IP address available");
return Status::Accept;
}
};
let session = context.data.session();
let config = &session.session_config.config;
if config.trusted_networks().contains(ip) {
if ip.is_loopback() {
debug!("accepted local connection");
} else {
debug!("accepted connection from trusted network address {ip}");
}
return Status::Accept;
}
let hostname = match config.hostname() {
Some(hostname) => hostname.into(),
None => context
.macros
.get_string(c_str!("j"))
.map_or_else(|| "unknown".into(), |h| h.into_owned()),
};
session.init_connection(hostname, ip);
Status::Continue
}
async fn handle_helo(context: &mut Context<AuthSession>, helo_host: CString) -> Status {
let session = context.data.session();
let helo_host = helo_host.to_string_lossy();
match session.authorize_helo(&mut context.reply, helo_host).await {
Ok(status) => status,
Err(e) => {
error!("failed to process HELO identity: {e}");
Status::Tempfail
}
}
}
async fn handle_mail(context: &mut Context<AuthSession>, smtp_args: Vec<CString>) -> Status {
let session = context.data.session();
let config = &session.session_config.config;
if config.trust_authenticated_senders() {
if let Some(login) = context.macros.get_string(c_str!("{auth_authen}")) {
debug!("accepted message from sender authenticated as \"{login}\"");
return Status::Accept;
}
}
let mail_from = smtp_args[0].to_string_lossy();
match session.authorize_mail_from(&mut context.reply, &mail_from).await {
Ok(status) => status,
Err(e) => {
error!("failed to process MAIL FROM identity: {e}");
Status::Tempfail
}
}
}
async fn handle_header(
context: &mut Context<AuthSession>,
name: CString,
value: CString,
) -> Status {
let name = name.to_string_lossy();
if name.eq_ignore_ascii_case(AuthenticationResultsHeader::NAME) {
let session = context.data.session();
let id = context.macros.queue_id();
let value = value.to_string_lossy();
session.process_auth_results_header(&id, &value);
}
Status::Continue
}
async fn handle_eom(context: &mut EomContext<AuthSession>) -> Status {
let session = context.data.session();
let id = context.macros.queue_id();
match session.finish_message(&context.actions, &id).await {
Ok(status) => status,
Err(e) => {
error!("failed to process message: {e}");
Status::Tempfail
}
}
}
async fn handle_abort(context: &mut Context<AuthSession>) -> Status {
let session = context.data.session();
session.abort_message();
Status::Continue
}
async fn handle_close(context: &mut Context<AuthSession>) -> Status {
context.data = None;
Status::Continue
}