Files
rezervovator/src/backend/mail.rs
T

262 lines
9.1 KiB
Rust
Raw Normal View History

2024-02-10 19:52:20 +01:00
use cfg_if::cfg_if;
use leptos::*;
use validator::Validate;
use crate::backend::data::{ApiResponse, Message, MessageType};
use crate::components::data_form::ForValidation;
cfg_if! { if #[cfg(feature = "ssr")] {
use sqlx::{PgPool, query, query_as};
use sqlx::Error;
use crate::backend::get_pool;
use crate::error::AppError;
use log::info;
use log::error;
2024-02-12 17:17:26 +01:00
use crate::config::Mailing;
use crate::config::MailTransport;
use crate::backend::data::ResSumWithItems;
use lettre::message::Message as LettreMessage;
use lettre::{FileTransport, SmtpTransport, Transport};
use lettre::transport::smtp::client::{Tls, TlsParameters};
2024-02-12 17:17:26 +01:00
use lettre::transport::smtp::authentication::Credentials;
2024-02-16 13:04:27 +01:00
use lettre::transport::smtp::extension::ClientId;
2024-02-12 17:17:26 +01:00
use std::ops::Add;
use std::sync::mpsc::Sender;
use std::sync::mpsc;
2024-02-10 19:52:20 +01:00
pub async fn message_for_type(msg_type: &MessageType, pool: &PgPool) -> Result<Message, Error> {
Ok(query_as::<_, Message>("SELECT * FROM message WHERE msg_type = $1")
.bind(msg_type)
.fetch_one(pool)
.await?)
}
pub async fn init_message(msg_type: &MessageType, pool: &PgPool) -> Result<(), Error> {
query("INSERT INTO message(msg_type, subject, text) VALUES($1, '', '')")
.bind(msg_type)
.execute(pool)
.await?;
Ok(())
}
pub async fn check_messages(pool: &PgPool) -> Result<(), AppError> {
let types = [
MessageType::NewReservation,
MessageType::NewReservationCust,
MessageType::ReservationApp,
MessageType::ReservationCanceled];
for msg_type in types {
let msg = message_for_type(&msg_type, pool).await;
if let Err(e) = msg {
if matches!(e, Error::RowNotFound) {
info!("Creating initial message for type {:?}", msg_type);
init_message(&msg_type, pool).await?;
} else {
return Err(e.into())
}
}
}
Ok(())
}
2024-02-12 17:17:26 +01:00
pub struct MailMessage {
reply_to: String,
to: String,
subject: String,
text: String
}
impl MailMessage {
pub fn new(reply_to: String, to: String, message: Message, reservation: &ResSumWithItems) -> Self {
Self {
reply_to,
to,
subject: message.subject,
text: Self::replace_body_vars(message.text, &reservation)
}
}
fn replace_body_vars(text: String, reservation: &ResSumWithItems) -> String {
text
.replace("#date#", &reservation.summary.date.format("%d. %m. %Y").to_string())
.replace("#summary#", &{
let mut sum = "".to_string();
for p in &reservation.reservations {
sum = sum.add(&format!("{}: {} - {}",
p.property.name,
p.reservation.from.format("%H:%M").to_string(), p.reservation.to.format("%H:%M").to_string()));
}
sum
}
)
}
pub fn build_mail(&self, from: String) -> Result<LettreMessage, AppError> {
Ok(LettreMessage::builder()
.from(from.parse()?)
.reply_to(self.reply_to.parse()?)
.to(self.to.parse()?)
.subject(&self.subject)
.body(self.text.clone())?)
}
}
#[derive(Clone)]
enum MailerType {
Smtp(SmtpTransport),
File(String)
}
#[derive(Clone)]
pub struct Mailer {
transport: MailerType,
from: String,
sender: Option<Sender<MailMessage>>
}
impl Mailer {
pub fn new(config: &Mailing) -> Self {
match config.transport() {
2024-02-12 17:17:26 +01:00
MailTransport::Smtp => {
let tls = if let Some(t) = config.accept_all_certs() {
if t {
let tls = TlsParameters::builder(config.server().clone().unwrap_or_default())
.dangerous_accept_invalid_certs(true)
.dangerous_accept_invalid_hostnames(true);
Some(tls.build().expect("Cannot build TLS params"))
} else {
None
}
} else {
None
};
let transport = if config.tls().unwrap_or(false) {
let transport = SmtpTransport::starttls_relay(&config.server().clone().unwrap_or_default())
.expect("Cannot create SMTP mail transport");
if let Some(t) = tls {
transport.tls(Tls::Required(t))
} else {
transport
}
2024-02-12 17:17:26 +01:00
} else {
let transport = SmtpTransport::relay(&config.server().clone().unwrap_or_default())
.expect("Cannot create SMTP mail transport");
if let Some(t) = tls {
transport.tls(Tls::Wrapper(t))
} else {
transport
}
};
let transport = if let Some(p) = config.port() {
2024-02-16 11:22:34 +01:00
transport.port(p)
} else {
transport
};
let transport = if let Some(hello) = config.hello_name() {
2024-02-16 13:04:27 +01:00
transport.hello_name(ClientId::Domain(hello.to_string()))
} else {
transport
};
if config.user().is_some() && config.password().is_some() {
let cred = Credentials::new(config.user().clone().unwrap(), config.password().clone().unwrap());
Self {
transport: MailerType::Smtp(transport.credentials(cred).build()),
from: config.from().to_string(),
sender: None
}
2024-02-12 17:17:26 +01:00
} else {
Self {
transport: MailerType::Smtp(transport.build()),
from: config.from().to_string(),
sender: None
}
2024-02-12 17:17:26 +01:00
}
}
MailTransport::File => {
Self {
transport: MailerType::File(config.path().clone().unwrap_or("".to_string())),
from: config.from().to_string(),
sender: None
}
2024-02-12 17:17:26 +01:00
}
}
}
fn send(&self, msg: &MailMessage) -> Result<(), AppError> {
let to_send = msg.build_mail(self.from.clone())?;
match &self.transport {
MailerType::Smtp(s) => {s.send(&to_send)?;},
MailerType::File(s) => {FileTransport::new(s).send(&to_send)?;}
}
2024-02-12 17:17:26 +01:00
Ok(())
}
pub fn start_sender(&mut self) {
let (tx, rx) = mpsc::channel::<MailMessage>();
self.sender = Some(tx);
let mailer = self.clone();
std::thread::spawn(move || {
loop {
let msg = rx.recv();
if let Err(e) = msg {
error!("Mailer error: {}", e.to_string());
break;
}
let msg = msg.unwrap();
if let Err(e) = mailer.send(&msg) {
error!("Mail send error: {}", e);
} else {
info!("Mail message for: {} has been successfully sent", msg.to);
}
}
});
info!("Mail sender started");
}
pub fn send_mail(&self, msg: MailMessage) -> Result<(), AppError> {
if let Err(e) = self.sender.as_ref().expect("Sender not started").send(msg) {
error!("Mail queue error: {}", e);
Err(AppError::MailSendError(e.to_string()))
} else {
info!("Message queued for send");
Ok(())
}
}
2024-02-12 17:17:26 +01:00
}
2024-02-10 19:52:20 +01:00
}}
#[server]
pub async fn get_message(msg_type: MessageType) -> Result<Message, ServerFnError> {
let pool = get_pool().await?;
Ok(message_for_type(&msg_type, &pool).await?)
}
#[server]
pub async fn update_message(message: Message) -> Result<ApiResponse<()>, ServerFnError> {
use crate::perm_check;
perm_check!(is_admin);
let pool = get_pool().await?;
query("UPDATE message SET subject = $1, text = $2 WHERE msg_type = $3")
.bind(message.subject)
.bind(message.text)
.bind(message.msg_type)
.execute(&pool)
.await?;
Ok(ApiResponse::Data(()))
}
impl ForValidation for UpdateMessage {
fn entity(&self) -> &dyn Validate {
&self.message
}
}