Skip to content

Commit 7dd5289

Browse files
committed
chore: build out the nsend newsletter endpoint
1 parent 7d77a09 commit 7dd5289

File tree

4 files changed

+88
-9
lines changed

4 files changed

+88
-9
lines changed

src/domain/subscriber_email.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ impl AsRef<str> for SubscriberEmail {
1818
}
1919
}
2020

21+
impl std::fmt::Display for SubscriberEmail {
22+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23+
// We just forward to the Display implementation of
24+
// the wrapped String.
25+
self.0.fmt(f)
26+
}
27+
}
28+
2129
#[cfg(test)]
2230
mod tests {
2331
use super::*;

src/email_client.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ impl EmailClient {
3030
impl EmailClient {
3131
pub async fn send_email(
3232
&self,
33-
recipient: SubscriberEmail,
33+
recipient: &SubscriberEmail,
3434
subject: &str,
3535
html_content: &str,
3636
text_content: &str,
@@ -141,7 +141,7 @@ mod tests {
141141

142142
let _ = email_client
143143
.send_email(
144-
recipient,
144+
&recipient,
145145
&generate_subject(),
146146
&generate_content(),
147147
&generate_content(),
@@ -164,7 +164,7 @@ mod tests {
164164

165165
let outcome = email_client
166166
.send_email(
167-
subscriber_email,
167+
&subscriber_email,
168168
&generate_subject(),
169169
&generate_content(),
170170
&generate_content(),
@@ -189,7 +189,7 @@ mod tests {
189189

190190
let outcome = email_client
191191
.send_email(
192-
subscriber_email,
192+
&subscriber_email,
193193
&generate_subject(),
194194
&generate_content(),
195195
&generate_content(),
@@ -216,7 +216,7 @@ mod tests {
216216

217217
let outcome = email_client
218218
.send_email(
219-
subscriber_email,
219+
&subscriber_email,
220220
&generate_subject(),
221221
&generate_content(),
222222
&generate_content(),

src/routes/newsletters.rs

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,88 @@
1-
use actix_web::{web, HttpResponse};
1+
use crate::{domain::SubscriberEmail, email_client::EmailClient, routes::error_chain_fmt};
2+
use actix_web::http::StatusCode;
3+
use actix_web::{web, HttpResponse, ResponseError};
4+
use anyhow::Context;
5+
use sqlx::PgPool;
26

37
#[derive(serde::Deserialize)]
48
pub struct BodyData {
59
title: String,
610
content: Content,
711
}
812

13+
struct ConfirmedSubscriber {
14+
email: SubscriberEmail,
15+
}
16+
17+
#[tracing::instrument(name = "Get confirmed subscribers", skip(pool))]
18+
async fn get_confirmed_subscribers(
19+
pool: &PgPool,
20+
) -> Result<Vec<Result<ConfirmedSubscriber, anyhow::Error>>, anyhow::Error> {
21+
let confirmed_subscriber =
22+
sqlx::query!(r#"SELECT email FROM subscriptions WHERE status = 'confirmed'"#,)
23+
.fetch_all(pool)
24+
.await?
25+
.into_iter()
26+
.map(|r| match SubscriberEmail::parse(r.email) {
27+
Ok(email) => Ok(ConfirmedSubscriber { email }),
28+
Err(error) => Err(anyhow::anyhow!(error)),
29+
})
30+
.collect();
31+
Ok(confirmed_subscriber)
32+
}
33+
934
#[derive(serde::Deserialize)]
1035
pub struct Content {
1136
text: String,
1237
html: String,
1338
}
1439

15-
pub async fn publish_newsletter(_body: web::Json<BodyData>) -> HttpResponse {
16-
HttpResponse::Ok().finish()
40+
#[derive(thiserror::Error)]
41+
pub enum PublishError {
42+
#[error(transparent)]
43+
UnexpectedError(#[from] anyhow::Error),
44+
}
45+
46+
impl ResponseError for PublishError {
47+
fn status_code(&self) -> StatusCode {
48+
match self {
49+
PublishError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
50+
}
51+
}
52+
}
53+
54+
impl std::fmt::Debug for PublishError {
55+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56+
error_chain_fmt(self, f)
57+
}
58+
}
59+
60+
pub async fn publish_newsletter(
61+
body: web::Json<BodyData>,
62+
pool: web::Data<PgPool>,
63+
email_client: web::Data<EmailClient>,
64+
) -> Result<HttpResponse, PublishError> {
65+
let subscribers = get_confirmed_subscribers(&pool).await?;
66+
for subscriber in subscribers {
67+
match subscriber {
68+
Ok(subscriber) => {
69+
email_client
70+
.send_email(
71+
&subscriber.email,
72+
&body.title,
73+
&body.content.text,
74+
&body.content.html,
75+
)
76+
.await
77+
.with_context(|| {
78+
format!("Failed to send newsletter issue to {}", subscriber.email)
79+
})?;
80+
}
81+
82+
Err(error) => {
83+
tracing::warn!("Skipping invalid subscriber: {}", error);
84+
}
85+
}
86+
}
87+
Ok(HttpResponse::Ok().finish())
1788
}

src/routes/subscriptions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ pub async fn send_confirmation_email(
136136

137137
email_client
138138
.send_email(
139-
new_subscriber.email,
139+
&new_subscriber.email,
140140
"Welcome!",
141141
&html_body_text,
142142
&plain_body_text,

0 commit comments

Comments
 (0)