aetheris-server 0.5.0

Authoritative heart and tick scheduler for the Aetheris multiplayer platform
Documentation
use async_trait::async_trait;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
use tracing::{debug, error, info};

#[async_trait]
pub trait EmailSender: Send + Sync {
    async fn send(
        &self,
        to: &str,
        subject: &str,
        plaintext: &str,
        html: &str,
    ) -> Result<(), String>;
}

pub struct LogEmailSender;

#[async_trait]
impl EmailSender for LogEmailSender {
    async fn send(
        &self,
        to: &str,
        subject: &str,
        plaintext: &str,
        _html: &str,
    ) -> Result<(), String> {
        info!("Sending email to: {} Subject: {}", to, subject);
        debug!("Body: {}", plaintext);
        Ok(())
    }
}

pub struct LettreSmtpEmailSender {
    transport: AsyncSmtpTransport<Tokio1Executor>,
    from: String,
}

impl LettreSmtpEmailSender {
    pub fn from_env() -> Result<Self, String> {
        let smtp_url = std::env::var("SMTP_URL").map_err(|_| "SMTP_URL missing")?;
        let smtp_user = std::env::var("SMTP_USERNAME").map_err(|_| "SMTP_USERNAME missing")?;
        let smtp_pass = std::env::var("SMTP_PASSWORD").map_err(|_| "SMTP_PASSWORD missing")?;
        let from = std::env::var("SMTP_FROM").map_err(|_| "SMTP_FROM missing")?;

        let creds = Credentials::new(smtp_user, smtp_pass);
        let transport = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp_url)
            .map_err(|e| e.to_string())?
            .credentials(creds)
            .build();

        Ok(Self { transport, from })
    }
}

#[async_trait]
impl EmailSender for LettreSmtpEmailSender {
    async fn send(
        &self,
        to: &str,
        subject: &str,
        plaintext: &str,
        html: &str,
    ) -> Result<(), String> {
        let email = Message::builder()
            .from(
                self.from
                    .parse()
                    .map_err(|e: lettre::address::AddressError| e.to_string())?,
            )
            .to(to
                .parse()
                .map_err(|e: lettre::address::AddressError| e.to_string())?)
            .subject(subject)
            .multipart(
                lettre::message::MultiPart::alternative()
                    .singlepart(lettre::message::SinglePart::plain(plaintext.to_string()))
                    .singlepart(lettre::message::SinglePart::html(html.to_string())),
            )
            .map_err(|e| e.to_string())?;

        let _ = self.transport.send(email).await.map_err(|e| {
            error!("Failed to send email: {}", e);
            e.to_string()
        })?;
        Ok(())
    }
}
pub struct ResendEmailSender {
    client: reqwest::Client,
    api_key: String,
    from: String,
}

impl ResendEmailSender {
    #[must_use]
    pub fn new(api_key: String, from: String) -> Self {
        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(10))
            .connect_timeout(std::time::Duration::from_secs(5))
            .pool_max_idle_per_host(2)
            .build()
            .unwrap_or_else(|_| reqwest::Client::new());

        Self {
            client,
            api_key,
            from,
        }
    }

    pub fn from_env() -> Result<Self, String> {
        let api_key = std::env::var("RESEND_API_KEY").map_err(|_| "RESEND_API_KEY missing")?;
        let from =
            std::env::var("RESEND_FROM").unwrap_or_else(|_| "onboarding@resend.dev".to_string());
        Ok(Self::new(api_key, from))
    }
}

#[async_trait]
impl EmailSender for ResendEmailSender {
    async fn send(
        &self,
        to: &str,
        subject: &str,
        plaintext: &str,
        html: &str,
    ) -> Result<(), String> {
        let body = serde_json::json!({
            "from": self.from,
            "to": [to],
            "subject": subject,
            "text": plaintext,
            "html": html,
        });

        let response = self
            .client
            .post("https://api.resend.com/emails")
            .header("Authorization", format!("Bearer {}", self.api_key))
            .json(&body)
            .send()
            .await
            .map_err(|e| e.to_string())?;

        if response.status().is_success() {
            Ok(())
        } else {
            let status = response.status();
            let error_text = response.text().await.unwrap_or_default();
            error!("Resend API error ({status}): {error_text}");
            Err(format!("Resend API error ({status}): {error_text}"))
        }
    }
}