From 862e2d6709be8d652de282260cf40a0baa738ddc Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 27 May 2025 13:59:06 +0000 Subject: [PATCH] feat: add mail client Untested --- Cargo.toml | 1 + src/api/v1/me/mod.rs | 6 +---- src/config.rs | 24 ++++++++++++++++++ src/error.rs | 5 ++++ src/main.rs | 7 ++++++ src/structs.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7a062d8..af5b2ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-con diesel_migrations = { version = "2.2.0", features = ["postgres"] } thiserror = "2.0.12" actix-multipart = "0.7.2" +lettre = { version = "0.11.16", features = ["tokio1", "tokio1-native-tls"] } [dependencies.tokio] version = "1.44" diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index 14067c3..58eaa02 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -33,7 +33,7 @@ pub async fn get(req: HttpRequest, data: web::Data) -> Result, display_name: Option, - password: Option, + //password: Option, will probably be handled through a reset password link email: Option, } @@ -83,10 +83,6 @@ pub async fn update( todo!(); } - if let Some(password) = &new_info.password { - todo!(); - } - if let Some(email) = &new_info.email { todo!(); } diff --git a/src/config.rs b/src/config.rs index 079ce35..4d2e96b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use crate::error::Error; use bunny_api_tokio::edge_storage::Endpoint; +use lettre::transport::smtp::authentication::Credentials; use log::debug; use serde::Deserialize; use tokio::fs::read_to_string; @@ -12,6 +13,7 @@ pub struct ConfigBuilder { web: Option, instance: Option, bunny: BunnyBuilder, + mail: Mail, } #[derive(Debug, Deserialize, Clone)] @@ -52,6 +54,20 @@ struct BunnyBuilder { cdn_url: Url, } +#[derive(Debug, Deserialize, Clone)] +pub struct Mail { + pub smtp: Smtp, + pub from: String, + pub tls: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Smtp { + pub server: String, + username: String, + password: String, +} + impl ConfigBuilder { pub async fn load(path: String) -> Result { debug!("loading config from: {}", path); @@ -101,6 +117,7 @@ impl ConfigBuilder { web, instance: self.instance.unwrap_or(Instance { registration: true }), bunny, + mail: self.mail, } } } @@ -112,6 +129,7 @@ pub struct Config { pub web: Web, pub instance: Instance, pub bunny: Bunny, + pub mail: Mail, } #[derive(Debug, Clone)] @@ -179,3 +197,9 @@ impl CacheDatabase { url } } + +impl Smtp { + pub fn credentials(&self) -> Credentials { + Credentials::new(self.username.clone(), self.password.clone()) + } +} diff --git a/src/error.rs b/src/error.rs index fb990c7..ca05c98 100644 --- a/src/error.rs +++ b/src/error.rs @@ -19,6 +19,7 @@ use serde_json::Error as JsonError; use thiserror::Error; use tokio::task::JoinError; use toml::de::Error as TomlError; +use lettre::{address::AddressError, transport::smtp::Error as SmtpError}; #[derive(Debug, Error)] pub enum Error { @@ -54,6 +55,10 @@ pub enum Error { PayloadError(#[from] PayloadError), #[error(transparent)] WsClosed(#[from] actix_ws::Closed), + #[error(transparent)] + SmtpError(#[from] SmtpError), + #[error(transparent)] + SmtpAddressError(#[from] AddressError), #[error("{0}")] PasswordHashError(String), #[error("{0}")] diff --git a/src/main.rs b/src/main.rs index 5ad1dc8..8bc1c68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; use error::Error; use simple_logger::SimpleLogger; +use structs::MailClient; use std::time::SystemTime; mod config; use config::{Config, ConfigBuilder}; @@ -40,6 +41,7 @@ pub struct Data { pub argon2: Argon2<'static>, pub start_time: SystemTime, pub bunny_cdn: bunny_api_tokio::Client, + pub mail_client: MailClient, } #[tokio::main] @@ -72,6 +74,10 @@ async fn main() -> Result<(), Error> { .init(bunny.api_key, bunny.endpoint, bunny.storage_zone) .await?; + let mail = config.mail.clone(); + + let mail_client = MailClient::new(mail.smtp.credentials(), mail.smtp.server, mail.from, mail.tls)?; + let database_url = config.database.url(); tokio::task::spawn_blocking(move || { @@ -112,6 +118,7 @@ async fn main() -> Result<(), Error> { argon2: Argon2::default(), start_time: SystemTime::now(), bunny_cdn, + mail_client, }; HttpServer::new(move || { diff --git a/src/structs.rs b/src/structs.rs index be4bd43..541d6ec 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -5,6 +5,8 @@ use diesel::{ update, }; use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; +use lettre::{message::{Mailbox, MessageBuilder as EmailBuilder}, transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message as Email, Tokio1Executor}; +use log::debug; use serde::{Deserialize, Serialize}; use tokio::task; use url::Url; @@ -35,6 +37,62 @@ fn load_or_empty( } } +#[derive(PartialEq, Eq, Clone)] +pub enum MailTls { + StartTls, + Tls, +} + +impl From for MailTls { + fn from(value: String) -> Self { + match &*value.to_lowercase() { + "starttls" => Self::StartTls, + _ => Self::Tls, + } + } +} + +#[derive(Clone)] +pub struct MailClient { + creds: Credentials, + smtp_server: String, + mbox: Mailbox, + tls: MailTls, +} + +impl MailClient { + pub fn new>(creds: Credentials, smtp_server: String, mbox: String, tls: T) -> Result { + Ok(Self { + creds, + smtp_server, + mbox: mbox.parse()?, + tls: tls.into(), + }) + } + + pub async fn message_builder(&self) -> EmailBuilder { + Email::builder() + .from(self.mbox.clone()) + } + + pub async fn send_mail(&self, email: Email) -> Result<(), Error> { + let mailer: AsyncSmtpTransport = match self.tls { + MailTls::StartTls => AsyncSmtpTransport::::starttls_relay(&self.smtp_server)? + .credentials(self.creds.clone()) + .build(), + MailTls::Tls => AsyncSmtpTransport::::relay(&self.smtp_server)? + .credentials(self.creds.clone()) + .build(), + }; + + let response = mailer.send(email).await?; + + debug!("mail sending response: {:?}", response); + + Ok(()) + } +} + #[derive(Queryable, Selectable, Insertable, Clone, Debug)] #[diesel(table_name = channels)] #[diesel(check_for_backend(diesel::pg::Pg))]