diff --git a/Cargo.lock b/Cargo.lock index 200af0d..a9031b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 63251be..6b24438 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/configuration/base.yaml b/configuration/base.yaml index 2a531b3..a5fed5a 100644 --- a/configuration/base.yaml +++ b/configuration/base.yaml @@ -6,3 +6,6 @@ database: username: "postgres" password: "password" name: "newsletter" +email_client: + base_url: "localhost" + sender_email: "test@fmail.com" diff --git a/configuration/production.yaml b/configuration/production.yaml index cd4608a..1d37bd6 100644 --- a/configuration/production.yaml +++ b/configuration/production.yaml @@ -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" diff --git a/src/configuration.rs b/src/configuration.rs index 492c126..e5512ba 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -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::parse(self.sender_email.clone()) + } } #[derive(serde::Deserialize)] diff --git a/src/domain/subscriber_email.rs b/src/domain/subscriber_email.rs index e28d153..f2ea9c2 100644 --- a/src/domain/subscriber_email.rs +++ b/src/domain/subscriber_email.rs @@ -1,6 +1,6 @@ use validator::validate_email; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SubscriberEmail(String); impl SubscriberEmail { diff --git a/src/email_client.rs b/src/email_client.rs new file mode 100644 index 0000000..551eb22 --- /dev/null +++ b/src/email_client.rs @@ -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!() + } +} diff --git a/src/lib.rs b/src/lib.rs index f948e62..0758966 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod configuration; pub mod domain; +pub mod email_client; pub mod routes; pub mod startup; pub mod telemetry; diff --git a/src/main.rs b/src/main.rs index 1dd1322..1ff7c2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(); } diff --git a/src/routes/subscriptions.rs b/src/routes/subscriptions.rs index a135509..c3ffd1c 100644 --- a/src/routes/subscriptions.rs +++ b/src/routes/subscriptions.rs @@ -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 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, Form(form): Form) -> Response { +pub async fn subscribe( + State(AppState { + db_pool, + email_client, + }): State, + Form(form): Form, +) -> 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, Form(form): Form) - #[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) } diff --git a/src/startup.rs b/src/startup.rs index 90bec1a..0b495e7 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -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, 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, Router> { + axum::serve(listener, app(connection, email_client).into_make_service()) } diff --git a/tests/health_check.rs b/tests/health_check.rs index 079df55..eea402e 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -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(); });