use crate::{domain::SubscriberEmail, routes::error_chain_fmt, startup::AppState}; use anyhow::Context; use axum::{ Json, extract::State, response::{IntoResponse, Response}, }; use reqwest::StatusCode; use sqlx::PgPool; #[derive(thiserror::Error)] pub enum PublishError { #[error(transparent)] UnexpectedError(#[from] anyhow::Error), } impl std::fmt::Debug for PublishError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { error_chain_fmt(self, f) } } impl IntoResponse for PublishError { fn into_response(self) -> Response { #[derive(serde::Serialize)] struct ErrorResponse<'a> { message: &'a str, } tracing::error!("{:?}", self); let status = match self { PublishError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR, }; let message = "An internal server error occured."; (status, Json(ErrorResponse { message })).into_response() } } #[derive(serde::Deserialize)] pub struct BodyData { title: String, content: Content, } #[derive(serde::Deserialize)] pub struct Content { html: String, text: String, } #[tracing::instrument( name = "Publishing a newsletter", skip(connection_pool, email_client, body) )] pub async fn publish_newsletter( State(AppState { connection_pool, email_client, .. }): State, body: Json, ) -> Result { let subscribers = get_confirmed_subscribers(&connection_pool).await?; for subscriber in subscribers { match subscriber { Ok(ConfirmedSubscriber { email, .. }) => { email_client .send_email(&email, &body.title, &body.content.html, &body.content.text) .await .with_context(|| { format!("Failed to send newsletter issue to {}", email.as_ref()) })?; } Err(e) => { tracing::warn!( "Skipping a confirmed subscriber. Their stored contact details are invalid: {}", e ) } } } Ok(StatusCode::OK.into_response()) } #[allow(dead_code)] struct ConfirmedSubscriber { name: String, email: SubscriberEmail, } #[tracing::instrument(name = "Get confirmed subscribers", skip(connection_pool))] async fn get_confirmed_subscribers( connection_pool: &PgPool, ) -> Result>, anyhow::Error> { let rows = sqlx::query!("SELECT name, email FROM subscriptions WHERE status = 'confirmed'") .fetch_all(connection_pool) .await?; let confirmed_subscribers = rows .into_iter() .map(|r| match SubscriberEmail::parse(r.email) { Ok(email) => Ok(ConfirmedSubscriber { name: r.name, email, }), Err(e) => Err(anyhow::anyhow!(e)), }) .collect(); Ok(confirmed_subscribers) }