/* * Copyright (C) 2025 Jonni Liljamo * * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for * more information. */ use async_trait::async_trait; use lettre::{ Message, SmtpTransport, Transport, message::{Mailbox, header::ContentType}, transport::smtp::authentication::Credentials, }; use serde::{Deserialize, Serialize}; use crate::{ config::{ServiceConfig, deserialize_token}, routes::message::MessageForm, }; use super::Service; #[derive(Clone, Deserialize, Serialize)] pub struct EmailConfig { from_name: Option, from: String, to_name: Option, to: String, username: String, #[serde(deserialize_with = "deserialize_token")] password: String, smtp_host: String, } #[typetag::serde(name = "email")] impl ServiceConfig for EmailConfig { fn as_any(&self) -> &dyn std::any::Any { self } } pub struct EmailService { config: EmailConfig, mailer: SmtpTransport, } impl EmailService { pub fn new( _client: reqwest::Client, config: EmailConfig, ) -> Result> { let credentials = Credentials::new(config.username.clone(), config.password.clone()); let mailer = SmtpTransport::relay(&config.smtp_host)? .credentials(credentials) .build(); Ok(Self { config, mailer }) } } #[async_trait] impl Service for EmailService { async fn send(&self, form: &MessageForm) -> Result<(), Box> { let message = Message::builder() .from(Mailbox::new( self.config.from_name.clone(), self.config.from.parse()?, )) .to(Mailbox::new( self.config.to_name.clone(), self.config.to.parse()?, )) .subject(form.title.clone()) .header(ContentType::TEXT_PLAIN) .body(form.message.clone())?; self.mailer.send(&message)?; Ok(()) } } #[cfg(test)] mod tests { use crate::config::Config; use super::*; #[tokio::test] async fn config() { Config::from_str( r#" [services.e] type = "email" from = "a@a.com" to = "b@b.com" username = "a" password = "123" smtp_host = "smtp.a.com" "#, ) .await .unwrap() .services["e"] .as_any() .downcast_ref::() .unwrap(); } }