feat: add mail client

Untested
This commit is contained in:
Radical 2025-05-27 13:59:06 +00:00
parent 16ccf94631
commit 862e2d6709
6 changed files with 96 additions and 5 deletions

View file

@ -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"

View file

@ -33,7 +33,7 @@ pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
struct NewInfo {
username: Option<String>,
display_name: Option<String>,
password: Option<String>,
//password: Option<String>, will probably be handled through a reset password link
email: Option<String>,
}
@ -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!();
}

View file

@ -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<WebBuilder>,
instance: Option<Instance>,
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<Self, Error> {
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())
}
}

View file

@ -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}")]

View file

@ -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 || {

View file

@ -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<T>(
}
}
#[derive(PartialEq, Eq, Clone)]
pub enum MailTls {
StartTls,
Tls,
}
impl From<String> 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<T: Into<MailTls>>(creds: Credentials, smtp_server: String, mbox: String, tls: T) -> Result<Self, Error> {
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<Tokio1Executor> = match self.tls {
MailTls::StartTls => AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&self.smtp_server)?
.credentials(self.creds.clone())
.build(),
MailTls::Tls => AsyncSmtpTransport::<Tokio1Executor>::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))]