feat: prepare email client usage

This commit is contained in:
Sandro Eiler 2024-02-21 11:18:44 +01:00
parent eecab50b55
commit 13db7853bd
12 changed files with 150 additions and 208 deletions

231
Cargo.lock generated
View file

@ -309,16 +309,6 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
@ -551,21 +541,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.0"
@ -914,16 +889,17 @@ dependencies = [
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
name = "hyper-rustls"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"bytes",
"futures-util",
"http 0.2.9",
"hyper 0.14.27",
"native-tls",
"rustls",
"tokio",
"tokio-native-tls",
"tokio-rustls",
]
[[package]]
@ -1216,24 +1192,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nix"
version = "0.27.1"
@ -1344,50 +1302,6 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "openssl"
version = "0.10.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.32",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "ordered-multimap"
version = "0.6.0"
@ -1708,25 +1622,27 @@ dependencies = [
"http 0.2.9",
"http-body 0.4.5",
"hyper 0.14.27",
"hyper-tls",
"hyper-rustls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots 0.22.6",
"winreg",
]
@ -1812,6 +1728,7 @@ version = "0.21.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
dependencies = [
"log",
"ring",
"rustls-webpki",
"sct",
@ -1848,15 +1765,6 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "schannel"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
dependencies = [
"windows-sys 0.42.0",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -1883,29 +1791,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "serde"
version = "1.0.193"
@ -2154,7 +2039,7 @@ dependencies = [
"tracing",
"url",
"uuid",
"webpki-roots",
"webpki-roots 0.25.3",
]
[[package]]
@ -2466,9 +2351,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.35.1"
version = "1.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
dependencies = [
"backtrace",
"bytes",
@ -2495,12 +2380,12 @@ dependencies = [
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"native-tls",
"rustls",
"tokio",
]
@ -2910,6 +2795,25 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
dependencies = [
"webpki",
]
[[package]]
name = "webpki-roots"
version = "0.25.3"
@ -2953,21 +2857,6 @@ dependencies = [
"windows-targets 0.52.0",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@ -3016,12 +2905,6 @@ dependencies = [
"windows_x86_64_msvc 0.52.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
@ -3034,12 +2917,6 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
@ -3052,12 +2929,6 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
@ -3070,12 +2941,6 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
@ -3088,12 +2953,6 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
@ -3106,12 +2965,6 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
@ -3124,12 +2977,6 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"

View file

@ -53,8 +53,12 @@ features = [
"migrate",
]
[dependencies.reqwest]
version = "0.11"
default-features = false
features = ["json", "rustls-tls"]
[dev-dependencies]
reqwest = "0.11"
once_cell = "1"
claims = "0.7"
fake = "2.9.2"

View file

@ -6,3 +6,6 @@ database:
username: "postgres"
password: "password"
name: "newsletter"
email_client:
base_url: "localhost"
sender_email: "test@fmail.com"

View file

@ -2,3 +2,6 @@ application:
host: 0.0.0.0
database:
require_ssl: true
email_client:
base_url: "https://api.postmarkapp.com"
sender_email: "sandro.eiler@uni-ulm.de"

View file

@ -4,6 +4,8 @@ use sqlx::postgres::PgConnectOptions;
use sqlx::postgres::PgSslMode;
use sqlx::ConnectOptions;
use crate::domain::SubscriberEmail;
#[derive(serde::Deserialize)]
/// The setting collection.
///
@ -12,6 +14,19 @@ use sqlx::ConnectOptions;
pub struct Settings {
pub database: DatabaseSettings,
pub application: ApplicationSettings,
pub email_client: EmailClientSettings,
}
#[derive(serde::Deserialize)]
pub struct EmailClientSettings {
pub base_url: String,
pub sender_email: String,
}
impl EmailClientSettings {
pub fn sender(&self) -> Result<SubscriberEmail, String> {
SubscriberEmail::parse(self.sender_email.clone())
}
}
#[derive(serde::Deserialize)]

View file

@ -1,6 +1,6 @@
use validator::validate_email;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct SubscriberEmail(String);
impl SubscriberEmail {

29
src/email_client.rs Normal file
View file

@ -0,0 +1,29 @@
use crate::domain::SubscriberEmail;
use reqwest::Client;
#[derive(Clone)]
pub struct EmailClient {
http_client: Client,
base_url: String,
sender: SubscriberEmail,
}
impl EmailClient {
pub fn new(base_url: String, sender: SubscriberEmail) -> Self {
Self {
http_client: Client::new(),
base_url,
sender,
}
}
pub async fn send_email(
&self,
recipient: SubscriberEmail,
subject: &str,
html_content: &str,
text_content: &str,
) -> Result<(), String> {
todo!()
}
}

View file

@ -4,6 +4,7 @@
pub mod configuration;
pub mod domain;
pub mod email_client;
pub mod routes;
pub mod startup;
pub mod telemetry;

View file

@ -1,4 +1,5 @@
use learn_axum::configuration::get_configuration;
use learn_axum::email_client::EmailClient;
use learn_axum::startup;
use learn_axum::telemetry::{get_subscriber, init_subscriber};
use sqlx::postgres::PgPoolOptions;
@ -20,5 +21,12 @@ async fn main() {
let connection_pool = PgPoolOptions::new()
.acquire_timeout(std::time::Duration::from_secs(2))
.connect_lazy_with(configuration.database.with_db());
startup::run(listener, connection_pool).await.unwrap();
let sender_email = configuration
.email_client
.sender()
.expect("Invalid sender email address.");
let email_client = EmailClient::new(configuration.email_client.base_url, sender_email);
startup::run(listener, connection_pool, email_client)
.await
.unwrap();
}

View file

@ -1,5 +1,7 @@
use crate::domain::SubscriberEmail;
use crate::domain::{NewSubscriber, SubscriberName};
use crate::email_client;
use crate::startup::AppState;
use axum::extract::State;
use axum::routing::post;
use axum::Form;
@ -31,21 +33,27 @@ impl TryFrom<FormData> for NewSubscriber {
#[tracing::instrument(
name = "Adding a new subscriber",
skip(form, pool),
skip(form, db_pool, email_client),
fields(
request_id = %Uuid::new_v4(),
subscriber_email = %form.email,
subscriber_name = %form.name
)
)]
pub async fn subscribe(State(pool): State<PgPool>, Form(form): Form<FormData>) -> Response {
pub async fn subscribe(
State(AppState {
db_pool,
email_client,
}): State<AppState>,
Form(form): Form<FormData>,
) -> Response {
let new_subscriber = match form.try_into() {
Ok(form) => form,
Err(_) => {
return (StatusCode::BAD_REQUEST, "Invalid subscription.").into_response();
}
};
match insert_subscriber(&pool, &new_subscriber).await {
match insert_subscriber(&db_pool, &new_subscriber).await {
Ok(_) => {
return (StatusCode::OK,).into_response();
}
@ -57,10 +65,10 @@ pub async fn subscribe(State(pool): State<PgPool>, Form(form): Form<FormData>) -
#[tracing::instrument(
name = "Saving new subscriber details in the database",
skip(new_subscriber, pool)
skip(new_subscriber, db_pool)
)]
pub async fn insert_subscriber(
pool: &PgPool,
db_pool: &PgPool,
new_subscriber: &NewSubscriber,
) -> Result<(), sqlx::Error> {
let _ = sqlx::query!(
@ -75,7 +83,7 @@ pub async fn insert_subscriber(
)
// We use `get_ref` to get an immutable reference to the `PgConnection`
// wrapped by `web::Data`.
.execute(pool)
.execute(db_pool)
.await
.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
@ -84,8 +92,8 @@ pub async fn insert_subscriber(
Ok(())
}
pub fn routes_subscriptions(pool: PgPool) -> Router {
pub fn routes_subscriptions(state: AppState) -> Router {
Router::new()
.route("/subscriptions", post(subscribe))
.with_state(pool)
.with_state(state)
}

View file

@ -1,3 +1,4 @@
use crate::email_client::EmailClient;
use axum::http::Request;
use axum::routing::IntoMakeService;
use axum::serve::Serve;
@ -13,6 +14,12 @@ use tower_http::{
use tracing::Level;
use uuid::Uuid;
#[derive(Clone)]
pub struct AppState {
pub db_pool: PgPool,
pub email_client: EmailClient,
}
// from https://docs.rs/tower-http/0.2.5/tower_http/request_id/index.html#using-uuids
#[derive(Clone)]
struct MakeRequestUuid;
@ -28,10 +35,14 @@ impl MakeRequestId for MakeRequestUuid {
/// API routing
///
/// * `connection`: The postgres connection pool
pub fn app(connection: PgPool) -> Router {
pub fn app(db_connection: PgPool, email_client: EmailClient) -> Router {
let state = AppState {
db_pool: db_connection.clone(),
email_client: email_client.clone(),
};
Router::new()
.merge(crate::routes::routes_health_check())
.merge(crate::routes::routes_subscriptions(connection.clone()))
.merge(crate::routes::routes_subscriptions(state))
.layer(
// from https://docs.rs/tower-http/0.2.5/tower_http/request_id/index.html#using-trace
ServiceBuilder::new()
@ -53,6 +64,11 @@ pub fn app(connection: PgPool) -> Router {
///
/// * `listener`: The TCP listener
/// * `connection`: The postgres connection pool
pub fn run(listener: TcpListener, connection: PgPool) -> Serve<IntoMakeService<Router>, Router> {
axum::serve(listener, app(connection).into_make_service())
/// * `email_client`: The email client
pub fn run(
listener: TcpListener,
connection: PgPool,
email_client: EmailClient,
) -> Serve<IntoMakeService<Router>, Router> {
axum::serve(listener, app(connection, email_client).into_make_service())
}

View file

@ -1,4 +1,5 @@
use learn_axum::configuration::{get_configuration, DatabaseSettings};
use learn_axum::email_client::EmailClient;
use learn_axum::telemetry::{get_subscriber, init_subscriber};
use once_cell::sync::Lazy;
use sqlx::{Connection, Executor, PgConnection, PgPool};
@ -147,7 +148,14 @@ async fn spawn_app() -> TestApp {
configuration.database.name = Uuid::new_v4().to_string();
let connection_pool = configure_database(&configuration.database).await;
let service = learn_axum::startup::app(connection_pool.clone());
// TODO: remove code duplication
let sender_email = configuration
.email_client
.sender()
.expect("Invalid sender email address.");
let email_client = EmailClient::new(configuration.email_client.base_url, sender_email);
let service = learn_axum::startup::app(connection_pool.clone(), email_client);
tokio::spawn(async move {
axum::serve(listener, service).await.unwrap();
});