spf-milter 0.6.0

Milter for SPF verification
Documentation
// SPF Milter – milter for SPF verification
// Copyright © 2020–2023 David Bürgin <dbuergin@gluet.ch>
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation, either version 3 of the License, or (at your option) any later
// version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.

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 {
    // Get the configuration at the very beginning, then keep it unchanged for
    // the rest of the session by storing it in the context. This is necessary
    // because we make actions/options below dependent on configuration.
    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
}