vsmtp-rule-engine 2.0.1-rc.4

Next-gen MTA. Secured, Faster and Greener
Documentation
/*
 * vSMTP mail transfer agent
 * Copyright (C) 2022 viridIT SAS
 *
 * 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 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::{
    api::{
        EngineResult, {Context, SharedObject},
    },
    get_global,
};
use rhai::plugin::{
    mem, Dynamic, FnAccess, FnNamespace, ImmutableString, Module, NativeCallContext,
    PluginFunction, RhaiResult, TypeId,
};
use vsmtp_common::Address;

pub use envelop::*;

/// Functions to inspect and mutate the SMTP envelop.
#[rhai::plugin::export_module]
mod envelop {
    /// Rewrite the sender received from the `MAIL FROM` command.
    ///
    /// # Args
    ///
    /// * `new_addr` - the new string sender address to set.
    ///
    /// # Effective smtp stage
    ///
    /// `mail` and onwards.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     preq: [
    ///        action "rewrite envelop" || envelop::rw_mail_from("unknown@example.com"),
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[allow(clippy::needless_pass_by_value)]
    #[rhai_fn(name = "rw_mail_from", return_raw)]
    pub fn rewrite_mail_from_envelop_str(
        ncc: NativeCallContext,
        new_addr: &str,
    ) -> EngineResult<()> {
        super::rewrite_mail_from_envelop(&mut get_global!(ncc, ctx)?, new_addr)
    }

    /// Rewrite the sender received from the `MAIL FROM` command.
    ///
    /// # Args
    ///
    /// * `new_addr` - the new sender address to set.
    ///
    /// # Effective smtp stage
    ///
    /// `mail` and onwards.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     preq: [
    ///        action "rewrite envelop" || envelop::rw_mail_from(address("unknown@example.com")),
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[rhai_fn(name = "rw_mail_from", return_raw)]
    #[allow(clippy::needless_pass_by_value)]
    pub fn rewrite_mail_from_envelop_obj(
        ncc: NativeCallContext,
        new_addr: SharedObject,
    ) -> EngineResult<()> {
        super::rewrite_mail_from_envelop(&mut get_global!(ncc, ctx)?, &new_addr.to_string())
    }

    /// Replace a recipient received by a `RCPT TO` command.
    ///
    /// # Args
    ///
    /// * `old_addr` - the recipient to replace.
    /// * `new_addr` - the new address to use when replacing `old_addr`.
    ///
    /// # Effective smtp stage
    ///
    /// `rcpt` and onwards.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     preq: [
    ///        action "rewrite envelop" || envelop::rw_rcpt("john.doe@example.com", "john.main@example.com"),
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[allow(clippy::needless_pass_by_value)]
    #[rhai_fn(name = "rw_rcpt", return_raw)]
    pub fn rewrite_rcpt_str_str(
        ncc: NativeCallContext,
        old_addr: &str,
        new_addr: &str,
    ) -> EngineResult<()> {
        super::rewrite_rcpt(&mut get_global!(ncc, ctx)?, old_addr, new_addr)
    }

    /// Replace a recipient received by a `RCPT TO` command.
    ///
    /// # Args
    ///
    /// * `old_addr` - the recipient to replace.
    /// * `new_addr` - the new address to use when replacing `old_addr`.
    ///
    /// # Effective smtp stage
    ///
    /// `rcpt` and onwards.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     preq: [
    ///        action "rewrite envelop" || envelop::rw_rcpt(address("john.doe@example.com"), "john.main@example.com"),
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[rhai_fn(name = "rw_rcpt", return_raw)]
    #[allow(clippy::needless_pass_by_value)]
    pub fn rewrite_rcpt_obj_str(
        ncc: NativeCallContext,
        old_addr: SharedObject,
        new_addr: &str,
    ) -> EngineResult<()> {
        super::rewrite_rcpt(&mut get_global!(ncc, ctx)?, &old_addr.to_string(), new_addr)
    }

    /// Replace a recipient received by a `RCPT TO` command.
    ///
    /// # Args
    ///
    /// * `old_addr` - the recipient to replace.
    /// * `new_addr` - the new address to use when replacing `old_addr`.
    ///
    /// # Effective smtp stage
    ///
    /// `rcpt` and onwards.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     preq: [
    ///        action "rewrite envelop" || envelop::rw_rcpt("john.doe@example.com", address("john.main@example.com")),
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[rhai_fn(name = "rw_rcpt", return_raw)]
    #[allow(clippy::needless_pass_by_value)]
    pub fn rewrite_rcpt_str_obj(
        ncc: NativeCallContext,
        old_addr: &str,
        new_addr: SharedObject,
    ) -> EngineResult<()> {
        super::rewrite_rcpt(&mut get_global!(ncc, ctx)?, old_addr, &new_addr.to_string())
    }

    /// Replace a recipient received by a `RCPT TO` command.
    ///
    /// # Args
    ///
    /// * `old_addr` - the recipient to replace.
    /// * `new_addr` - the new address to use when replacing `old_addr`.
    ///
    /// # Effective smtp stage
    ///
    /// `rcpt` and onwards.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     preq: [
    ///        action "rewrite envelop" || envelop::rw_rcpt(address("john.doe@example.com"), address("john.main@example.com")),
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[rhai_fn(name = "rw_rcpt", return_raw)]
    #[allow(clippy::needless_pass_by_value)]
    pub fn rewrite_rcpt_obj_obj(
        ncc: NativeCallContext,
        old_addr: SharedObject,
        new_addr: SharedObject,
    ) -> EngineResult<()> {
        super::rewrite_rcpt(
            &mut get_global!(ncc, ctx)?,
            &old_addr.to_string(),
            &new_addr.to_string(),
        )
    }

    /// Add a new recipient to the envelop. Note that this does not add
    /// the recipient to the `To` header. Use `msg::add_rcpt` for that.
    ///
    /// # Args
    ///
    /// * `rcpt` - the new recipient to add.
    ///
    /// # Effective smtp stage
    ///
    /// All of them.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     connect: [
    ///        // always deliver a copy of the message to "john.doe@example.com".
    ///        action "rewrite envelop" || envelop::add_rcpt("john.doe@example.com"),
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[allow(clippy::needless_pass_by_value)]
    #[rhai_fn(name = "add_rcpt", return_raw)]
    pub fn add_rcpt_envelop_str(ncc: NativeCallContext, new_addr: &str) -> EngineResult<()> {
        super::add_rcpt_envelop(&mut get_global!(ncc, ctx)?, new_addr)
    }

    /// Add a new recipient to the envelop. Note that this does not add
    /// the recipient to the `To` header. Use `msg::add_rcpt` for that.
    ///
    /// # Args
    ///
    /// * `rcpt` - the new recipient to add.
    ///
    /// # Effective smtp stage
    ///
    /// All of them.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     connect: [
    ///        // always deliver a copy of the message to "john.doe@example.com".
    ///        action "rewrite envelop" || envelop::add_rcpt(address("john.doe@example.com")),
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[rhai_fn(name = "add_rcpt", return_raw)]
    #[allow(clippy::needless_pass_by_value)]
    pub fn add_rcpt_envelop_obj(
        ncc: NativeCallContext,
        new_addr: SharedObject,
    ) -> EngineResult<()> {
        super::add_rcpt_envelop(&mut get_global!(ncc, ctx)?, &new_addr.to_string())
    }

    /// Alias for `envelop::add_rcpt`.
    #[allow(clippy::needless_pass_by_value)]
    #[rhai_fn(name = "bcc", return_raw)]
    pub fn bcc_str(ncc: NativeCallContext, new_addr: &str) -> EngineResult<()> {
        super::add_rcpt_envelop_str(ncc, new_addr)
    }

    /// Alias for `envelop::add_rcpt`.
    #[allow(clippy::needless_pass_by_value)]
    #[rhai_fn(name = "bcc", return_raw)]
    pub fn bcc_obj(ncc: NativeCallContext, new_addr: SharedObject) -> EngineResult<()> {
        super::add_rcpt_envelop_obj(ncc, new_addr)
    }

    /// Remove a recipient from the envelop. Note that this does not remove
    /// the recipient from the `To` header. Use `msg::rm_rcpt` for that.
    ///
    /// # Args
    ///
    /// * `rcpt` - the recipient to remove.
    ///
    /// # Effective smtp stage
    ///
    /// All of them.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     preq: [
    ///        // never deliver to "john.doe@example.com".
    ///        action "rewrite envelop" || envelop::rm_rcpt("john.doe@example.com"),
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[allow(clippy::needless_pass_by_value)]
    #[rhai_fn(name = "rm_rcpt", return_raw)]
    pub fn remove_rcpt_envelop_str(ncc: NativeCallContext, addr: &str) -> EngineResult<()> {
        super::remove_rcpt_envelop(&mut get_global!(ncc, ctx)?, addr)
    }

    /// Remove a recipient from the envelop. Note that this does not remove
    /// the recipient from the `To` header. Use `msg::rm_rcpt` for that.
    ///
    /// # Args
    ///
    /// * `rcpt` - the recipient to remove.
    ///
    /// # Effective smtp stage
    ///
    /// All of them.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     preq: [
    ///        // never deliver to "john.doe@example.com".
    ///        action "rewrite envelop" || envelop::rm_rcpt(address("john.doe@example.com")),
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[rhai_fn(name = "rm_rcpt", return_raw)]
    #[allow(clippy::needless_pass_by_value)]
    pub fn remove_rcpt_envelop_obj(ncc: NativeCallContext, addr: SharedObject) -> EngineResult<()> {
        super::remove_rcpt_envelop(&mut get_global!(ncc, ctx)?, &addr.to_string())
    }
}

fn rewrite_mail_from_envelop(context: &mut Context, new_addr: &str) -> EngineResult<()> {
    vsl_guard_ok!(context.write())
        .set_reverse_path(Some(vsl_conversion_ok!(
            "address",
            <Address as std::str::FromStr>::from_str(new_addr)
        )))
        .map_err(|e| e.to_string().into())
}

fn rewrite_rcpt(context: &mut Context, old_addr: &str, new_addr: &str) -> EngineResult<()> {
    let old_addr = vsl_conversion_ok!(
        "address",
        <Address as std::str::FromStr>::from_str(old_addr)
    );
    let new_addr = vsl_conversion_ok!(
        "address",
        <Address as std::str::FromStr>::from_str(new_addr)
    );

    let mut context = vsl_guard_ok!(context.write());
    context
        .remove_forward_path(&old_addr)
        .map_err::<Box<rhai::EvalAltResult>, _>(|e| e.to_string().into())?;
    context
        .add_forward_path(new_addr)
        .map_err::<Box<rhai::EvalAltResult>, _>(|e| e.to_string().into())?;

    Ok(())
}

fn add_rcpt_envelop(context: &mut Context, new_addr: &str) -> EngineResult<()> {
    vsl_guard_ok!(context.write())
        .add_forward_path(vsl_conversion_ok!(
            "address",
            <Address as std::str::FromStr>::from_str(new_addr)
        ))
        .map_err(|err| format!("failed to run `add_rcpt_envelop`: {err}").into())
}

fn remove_rcpt_envelop(context: &mut Context, addr: &str) -> EngineResult<()> {
    let addr = vsl_conversion_ok!("address", <Address as std::str::FromStr>::from_str(addr));

    let mut context = vsl_guard_ok!(context.write());
    context
        .remove_forward_path(&addr)
        .map_err::<Box<rhai::EvalAltResult>, _>(|e| e.to_string().into())?;
    Ok(())
}