use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use chrono::Utc;
use sea_query::{Expr, Iden, Order, Query};
use sqlx::Row;
use super::{Envelope, Mailer};
use crate::error::{CoreError, CoreResult};
use crate::query::{bind_values, build, insert_returning_id, AnyRowExt};
use crate::queue::time_text;
use crate::Db;
#[derive(Iden)]
pub(crate) enum MailLog {
Table,
Id,
Recipients,
Subject,
Text,
Html,
SentAt,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoggedMail {
pub id: i64,
pub recipients: String,
pub subject: String,
pub text: String,
pub html: Option<String>,
pub sent_at: String,
}
#[derive(Clone)]
pub struct LogMailer {
db: Db,
}
impl LogMailer {
pub fn new(db: Db) -> Self {
Self { db }
}
pub async fn recent(db: &Db, limit: u64) -> CoreResult<Vec<LoggedMail>> {
let (sql, values) = build(
db.backend,
Query::select()
.columns([
MailLog::Id,
MailLog::Recipients,
MailLog::Subject,
MailLog::Text,
MailLog::Html,
MailLog::SentAt,
])
.from(MailLog::Table)
.order_by(MailLog::Id, Order::Desc)
.limit(limit)
.to_owned(),
);
let rows = bind_values(sqlx::query(&sql), values)
.fetch_all(&db.pool)
.await?;
let mut out = Vec::with_capacity(rows.len());
for r in rows {
out.push(LoggedMail {
id: r.try_get::<i64, _>("id")?,
recipients: r.get_text("recipients")?,
subject: r.get_text("subject")?,
text: r.get_text("text")?,
html: r.get_text_opt("html")?,
sent_at: r.get_text("sent_at")?,
});
}
Ok(out)
}
pub async fn clear(db: &Db) -> CoreResult<u64> {
let (sql, values) = build(
db.backend,
Query::delete().from_table(MailLog::Table).to_owned(),
);
let done = bind_values(sqlx::query(&sql), values)
.execute(&db.pool)
.await?;
Ok(done.rows_affected())
}
}
fn recipients_of(envelope: &Envelope) -> String {
let mut all: Vec<String> = envelope.to.iter().map(ToString::to_string).collect();
all.extend(envelope.cc.iter().map(|a| format!("cc: {a}")));
all.extend(envelope.bcc.iter().map(|a| format!("bcc: {a}")));
all.join(", ")
}
#[async_trait]
impl Mailer for LogMailer {
fn name(&self) -> &'static str {
"log"
}
async fn deliver(&self, envelope: &Envelope) -> CoreResult<()> {
let recipients = recipients_of(envelope);
tracing::info!(
to = %recipients,
subject = %envelope.subject,
"mail kept in the log"
);
let stmt = Query::insert()
.into_table(MailLog::Table)
.columns([
MailLog::Recipients,
MailLog::Subject,
MailLog::Text,
MailLog::Html,
MailLog::SentAt,
])
.values_panic([
recipients.into(),
envelope.subject.as_str().into(),
envelope.text.as_str().into(),
envelope.html.clone().into(),
time_text(Utc::now()).into(),
])
.to_owned();
insert_returning_id(&self.db, stmt, MailLog::Id)
.await
.map_err(CoreError::from)?;
Ok(())
}
}
#[derive(Clone, Default)]
pub struct StubMailer {
sent: Arc<Mutex<Vec<Envelope>>>,
failure: Arc<Mutex<Option<String>>>,
}
impl StubMailer {
pub fn new() -> Self {
Self::default()
}
pub fn sent(&self) -> Vec<Envelope> {
self.sent.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
pub fn fail_with(&self, message: Option<&str>) {
*self.failure.lock().unwrap_or_else(|e| e.into_inner()) = message.map(str::to_string);
}
}
#[async_trait]
impl Mailer for StubMailer {
fn name(&self) -> &'static str {
"stub"
}
async fn deliver(&self, envelope: &Envelope) -> CoreResult<()> {
if let Some(message) = self
.failure
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
{
return Err(CoreError::Internal(message));
}
self.sent
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(envelope.clone());
Ok(())
}
}
#[allow(dead_code)]
fn _uses(_: Expr) {}