feat: direct email notification endpoint

This commit is contained in:
sychic
2026-06-02 17:09:50 +01:00
committed by Calum H. (IMB11)
parent e41d116213
commit cd7dced0a6
2 changed files with 136 additions and 8 deletions
@@ -129,12 +129,11 @@ impl NotificationBuilder {
Ok(())
}
pub async fn insert_many(
async fn insert_many_records(
&self,
users: Vec<DBUserId>,
users: &[DBUserId],
transaction: &mut PgTransaction<'_>,
redis: &RedisPool,
) -> Result<(), DatabaseError> {
) -> Result<Vec<i64>, DatabaseError> {
let notification_ids =
generate_many_notification_ids(users.len(), &mut *transaction)
.await?;
@@ -163,6 +162,20 @@ impl NotificationBuilder {
.execute(&mut *transaction)
.await?;
Ok(notification_ids)
}
pub async fn insert_many(
&self,
users: Vec<DBUserId>,
transaction: &mut PgTransaction<'_>,
redis: &RedisPool,
) -> Result<(), DatabaseError> {
let notification_ids =
self.insert_many_records(&users, transaction).await?;
let users_raw_ids = users.iter().map(|x| x.0).collect::<Vec<_>>();
let notification_types = notification_ids
.iter()
.map(|_| self.body.notification_type().as_str())
@@ -181,6 +194,19 @@ impl NotificationBuilder {
Ok(())
}
/// Like [`insert_many`], but skips queuing deliveries so the caller can
/// manually send the notifications.
pub async fn insert_many_without_delivery(
&self,
users: Vec<DBUserId>,
transaction: &mut PgTransaction<'_>,
redis: &RedisPool,
) -> Result<(), DatabaseError> {
self.insert_many_records(&users, transaction).await?;
DBNotification::clear_user_notifications_cache(&users, redis).await?;
Ok(())
}
pub async fn insert_many_deliveries(
transaction: &mut PgTransaction<'_>,
redis: &RedisPool,
@@ -5,19 +5,28 @@ use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::user_item::DBUser;
use crate::database::redis::RedisPool;
use crate::models::users::Role;
use crate::models::v3::notifications::NotificationBody;
use crate::models::v3::notifications::{
NotificationBody, NotificationDeliveryStatus,
};
use crate::models::v3::pats::Scopes;
use crate::queue::email::EmailQueue;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::util::guards::external_notification_key_guard;
use actix_web::HttpRequest;
use actix_web::http::StatusCode;
use actix_web::web;
use actix_web::{HttpResponse, post};
use actix_web::{
CustomizeResponder, HttpRequest, HttpResponse, Responder, post,
};
use ariadne::ids::UserId;
use eyre::eyre;
use lettre::message::Mailbox;
use serde::Deserialize;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(create).service(send_custom_email);
cfg.service(create)
.service(create_direct_email)
.service(send_custom_email);
}
#[derive(Deserialize)]
@@ -56,6 +65,99 @@ pub async fn create(
Ok(HttpResponse::Accepted().finish())
}
/// Directly sends emails to users and inserts notifications when emails are
/// delivered successfully.
///
/// Responds with the user IDs that could not be emailed:
/// - `200` if every recipient was delivered (empty list)
/// - `207` if some recipients failed (list of failed IDs)
/// - `500` if no recipient was delivered
#[post(
"external_notifications/direct-email",
guard = "external_notification_key_guard"
)]
pub async fn create_direct_email(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
email_queue: web::Data<EmailQueue>,
create_notification: web::Json<CreateNotification>,
) -> Result<CustomizeResponder<web::Json<Vec<UserId>>>, ApiError> {
let CreateNotification { body, user_ids } =
create_notification.into_inner();
let user_ids = user_ids
.into_iter()
.map(|x| DBUserId(x.0 as i64))
.collect::<Vec<_>>();
let mut txn = pool.begin().await?;
if !DBUser::exists_many(&user_ids, &mut txn).await? {
return Err(ApiError::InvalidInput(
"One of the specified users do not exist.".to_owned(),
));
}
let mut results: Vec<Result<DBUserId, DBUserId>> =
Vec::with_capacity(user_ids.len());
for user_id in &user_ids {
let user = DBUser::get_id(*user_id, &mut txn, &redis).await?.ok_or(
ApiError::Internal(eyre!(
"user `{}` disappeared while sending notification email",
user_id.0
)),
)?;
let delivered =
match user.email.and_then(|email| email.parse::<Mailbox>().ok()) {
Some(mailbox) => {
email_queue
.send_one(&mut txn, body.clone(), *user_id, mailbox)
.await?
== NotificationDeliveryStatus::Delivered
}
None => false,
};
results.push(if delivered {
Ok(*user_id)
} else {
Err(*user_id)
});
}
let delivered = results
.iter()
.filter_map(|result| result.as_ref().ok().copied())
.collect::<Vec<_>>();
if delivered.is_empty() {
return Err(ApiError::Internal(eyre!(
"failed to deliver notification email to any of {} recipients",
user_ids.len(),
)));
}
NotificationBuilder { body }
.insert_many_without_delivery(delivered, &mut txn, &redis)
.await?;
txn.commit().await?;
let failed = results
.into_iter()
.filter_map(|result| result.err().map(|id| UserId(id.0 as u64)))
.collect::<Vec<_>>();
let status = if failed.is_empty() {
StatusCode::OK
} else {
StatusCode::MULTI_STATUS
};
Ok(web::Json(failed).customize().with_status(status))
}
#[derive(Deserialize)]
struct SendEmail {
pub users: Vec<UserId>,