rok-mail 0.6.0

Email support for the rok ecosystem — Mailable trait, log/SMTP drivers
Documentation
mod drivers;
mod mail;

pub mod config;
pub mod error;
pub mod errors;
pub mod mailable;

#[cfg(feature = "axum")]
pub mod mail_layer;

#[cfg(feature = "queue")]
pub mod queue_job;

pub use config::MailConfig;
pub use error::MailError;
pub use mail::{global_config, set_global_config, Mail};
pub use mailable::Mailable;

#[cfg(feature = "markdown")]
pub use mailable::MarkdownMailable;

#[cfg(feature = "axum")]
pub use mail_layer::MailLayer;

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use crate::{Mail, MailConfig, MailError, Mailable};

    struct Welcome {
        name: String,
    }

    impl Mailable for Welcome {
        fn subject(&self) -> &str {
            "Welcome!"
        }
        fn body(&self) -> String {
            format!("Hi, {}!", self.name)
        }
        fn html_body(&self) -> Option<String> {
            Some(format!("<p>Hi, <b>{}</b>!</p>", self.name))
        }
    }

    fn log_config() -> MailConfig {
        MailConfig {
            driver: "log".into(),
            ..MailConfig::default()
        }
    }

    #[test]
    fn mailable_subject_and_body() {
        let m = Welcome {
            name: "Alice".into(),
        };
        assert_eq!(m.subject(), "Welcome!");
        assert!(m.body().contains("Alice"));
        assert!(m.html_body().unwrap().contains("Alice"));
    }

    #[test]
    fn mail_config_defaults() {
        let c = MailConfig::default();
        assert_eq!(c.driver, "log");
        assert_eq!(c.from_address, "noreply@rok-app.com");
        assert_eq!(c.smtp_port, 587);
        assert_eq!(c.smtp_encryption, "starttls");
        assert!(c.postmark_api_key.is_none());
        assert!(c.resend_api_key.is_none());
    }

    #[tokio::test]
    async fn send_with_log_driver_succeeds() {
        let result = Mail::send_with(
            Welcome { name: "Bob".into() },
            "bob@example.com",
            &log_config(),
        )
        .await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn send_with_unknown_driver_returns_error() {
        let config = MailConfig {
            driver: "carrier_pigeon".into(),
            ..MailConfig::default()
        };
        let result = Mail::send_with(
            Welcome {
                name: "Carol".into(),
            },
            "carol@example.com",
            &config,
        )
        .await;
        assert!(matches!(result, Err(MailError::UnknownDriver(d)) if d == "carrier_pigeon"));
    }

    #[tokio::test]
    async fn send_without_layer_returns_not_configured() {
        let result = Mail::send(
            Welcome {
                name: "Dave".into(),
            },
            "dave@example.com",
        )
        .await;
        assert!(matches!(result, Err(MailError::NotConfigured)));
    }
}