diff --git a/Cargo.lock b/Cargo.lock index b3fc8a5c66..ef9fef98f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,6 +485,16 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.2", + "stacker", +] + [[package]] name = "clap" version = "4.5.1" @@ -793,6 +803,22 @@ dependencies = [ "serde", ] +[[package]] +name = "email-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +dependencies = [ + "base64 0.21.5", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -1278,7 +1304,7 @@ dependencies = [ "rustls 0.21.8", "rustls-native-certs", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", ] [[package]] @@ -1343,6 +1369,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "ignore" version = "0.4.22" @@ -1467,6 +1503,35 @@ dependencies = [ "spin 0.5.2", ] +[[package]] +name = "lettre" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357ff5edb6d8326473a64c82cf41ddf78ab116f89668c50c4fac1b321e5e80f4" +dependencies = [ + "async-trait", + "base64 0.21.5", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "httpdate", + "idna 0.5.0", + "mime", + "nom", + "percent-encoding 2.3.0", + "quoted_printable", + "rustls 0.22.2", + "rustls-pemfile 2.1.0", + "socket2 0.5.5", + "tokio", + "tokio-rustls 0.25.0", + "url 2.4.1", + "webpki-roots 0.26.1", +] + [[package]] name = "libc" version = "0.2.149" @@ -1646,6 +1711,7 @@ dependencies = [ "google-fcm1", "job-executor", "jsonwebtoken", + "lettre", "prost 0.12.3", "rand", "reqwest", @@ -2151,6 +2217,15 @@ dependencies = [ "prost 0.12.3", ] +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + [[package]] name = "quote" version = "1.0.35" @@ -2160,6 +2235,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0" + [[package]] name = "rand" version = "0.8.5" @@ -2267,14 +2348,14 @@ dependencies = [ "percent-encoding 2.3.0", "pin-project-lite", "rustls 0.21.8", - "rustls-pemfile", + "rustls-pemfile 1.0.3", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "system-configuration", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tower-service", "url 2.4.1", "wasm-bindgen", @@ -2421,7 +2502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 1.0.3", "schannel", "security-framework", ] @@ -2435,11 +2516,21 @@ dependencies = [ "base64 0.21.5", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c333bb734fcdedcea57de1602543590f545f127dc8b533324318fd492c5c70b" +dependencies = [ + "base64 0.21.5", + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a716eb65e3158e90e17cd93d855216e27bde02745ab842f2cab4a39dba1bacf" +checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7" [[package]] name = "rustls-webpki" @@ -2858,7 +2949,7 @@ dependencies = [ "paste", "percent-encoding 2.3.0", "rustls 0.21.8", - "rustls-pemfile", + "rustls-pemfile 1.0.3", "serde", "serde_json", "sha2", @@ -3056,6 +3147,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stacker" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "winapi", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -3301,6 +3405,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.2", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -3893,6 +4008,15 @@ version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +[[package]] +name = "webpki-roots" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "4.4.2" @@ -4128,7 +4252,7 @@ dependencies = [ "log", "percent-encoding 2.3.0", "rustls 0.22.2", - "rustls-pemfile", + "rustls-pemfile 1.0.3", "seahash", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 82bedab39d..22e9902609 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,4 +46,5 @@ prost = "0.12" rust-i18n = "3" google-fcm1 = "5.0.3" sqlxmq = { version = "0.5", default-features = false, features = ["runtime-tokio-rustls"] } +lettre = { version = "0.11.4", default-features = false, features = ["builder", "tokio1", "tokio1-rustls-tls", "smtp-transport"] } diff --git a/core/notifications/BUCK b/core/notifications/BUCK index 0c4417452e..e03a49c3a6 100644 --- a/core/notifications/BUCK +++ b/core/notifications/BUCK @@ -69,6 +69,7 @@ galoy_rust_bin( "//third-party/rust:prost", "//third-party/rust:rust-i18n", "//third-party/rust:google-fcm1", + "//third-party/rust:lettre", ], extra_tests = [ "//lib/tracing-rs:tracing", diff --git a/core/notifications/Cargo.toml b/core/notifications/Cargo.toml index 80fbbdf837..2d4de783af 100644 --- a/core/notifications/Cargo.toml +++ b/core/notifications/Cargo.toml @@ -44,6 +44,7 @@ tonic-health = { workspace = true } prost = { workspace = true } rust-i18n = { workspace = true } google-fcm1 = { workspace = true } +lettre = { workspace = true } [build-dependencies] tonic-build = { workspace = true } diff --git a/core/notifications/notifications.yml b/core/notifications/notifications.yml index 6c0dda80eb..b00555acd6 100644 --- a/core/notifications/notifications.yml +++ b/core/notifications/notifications.yml @@ -6,8 +6,13 @@ # grpc_server: # port: 6685 app: - executor: + push_executor: fcm: google_application_credentials_path: "./config/notifications/fake_service_account.json" + email_executor: + smtp: + username: "" + from_email: "" + relay: "" # kratos_import: # execute_import: true diff --git a/core/notifications/src/app/config.rs b/core/notifications/src/app/config.rs index 3d5f8d0217..39d3165f3e 100644 --- a/core/notifications/src/app/config.rs +++ b/core/notifications/src/app/config.rs @@ -1,8 +1,9 @@ use serde::{Deserialize, Serialize}; -use crate::push_executor::PushExecutorConfig; +use crate::{email_executor::EmailExecutorConfig, push_executor::PushExecutorConfig}; #[derive(Clone, Default, Serialize, Deserialize)] pub struct AppConfig { - pub executor: PushExecutorConfig, + pub push_executor: PushExecutorConfig, + pub email_executor: EmailExecutorConfig, } diff --git a/core/notifications/src/app/error.rs b/core/notifications/src/app/error.rs index 1b8c5dbbe4..5358748ba1 100644 --- a/core/notifications/src/app/error.rs +++ b/core/notifications/src/app/error.rs @@ -1,7 +1,8 @@ use thiserror::Error; use crate::{ - job::error::JobError, push_executor::error::PushExecutorError, + email_executor::error::EmailExecutorError, job::error::JobError, + push_executor::error::PushExecutorError, user_notification_settings::error::UserNotificationSettingsError, }; @@ -14,5 +15,7 @@ pub enum ApplicationError { #[error("{0}")] PushExecutorError(#[from] PushExecutorError), #[error("{0}")] + EmailExecutorError(#[from] EmailExecutorError), + #[error("{0}")] Sqlx(#[from] sqlx::Error), } diff --git a/core/notifications/src/app/mod.rs b/core/notifications/src/app/mod.rs index f6ca62e049..c7e028c708 100644 --- a/core/notifications/src/app/mod.rs +++ b/core/notifications/src/app/mod.rs @@ -8,7 +8,8 @@ use tracing::instrument; use std::sync::Arc; use crate::{ - job, notification_event::*, primitives::*, push_executor::*, user_notification_settings::*, + email_executor::EmailExecutor, job, notification_event::*, primitives::*, push_executor::*, + user_notification_settings::*, }; pub use config::*; @@ -25,8 +26,10 @@ pub struct NotificationsApp { impl NotificationsApp { pub async fn init(pool: Pool, config: AppConfig) -> Result { let settings = UserNotificationSettingsRepo::new(&pool); - let executor = PushExecutor::init(config.executor.clone(), settings.clone()).await?; - let runner = job::start_job_runner(&pool, executor).await?; + let push_executor = + PushExecutor::init(config.push_executor.clone(), settings.clone()).await?; + let email_executor = EmailExecutor::init(config.email_executor.clone(), settings.clone())?; + let runner = job::start_job_runner(&pool, push_executor, email_executor).await?; Ok(Self { _config: config, pool, @@ -173,6 +176,9 @@ impl NotificationsApp { event: T, ) -> Result<(), ApplicationError> { let mut tx = self.pool.begin().await?; + if event.should_send_email() { + job::spawn_send_email_notification(&mut tx, event.clone().into()).await?; + } job::spawn_send_push_notification(&mut tx, event.into()).await?; tx.commit().await?; Ok(()) diff --git a/core/notifications/src/cli/config.rs b/core/notifications/src/cli/config.rs index 8a9a2e8880..cc973c5b0e 100644 --- a/core/notifications/src/cli/config.rs +++ b/core/notifications/src/cli/config.rs @@ -34,6 +34,7 @@ fn default_tracing_config() -> TracingConfig { pub struct EnvOverride { pub db_con: String, pub kratos_pg_con: Option, + pub email_password: String, } impl Config { @@ -41,6 +42,7 @@ impl Config { path: Option>, EnvOverride { db_con, + email_password, kratos_pg_con, }: EnvOverride, ) -> anyhow::Result { @@ -53,8 +55,9 @@ impl Config { }; config.db.pg_con = db_con; config.kratos_import.pg_con = kratos_pg_con; + config.app.email_executor.smtp.password = email_password; - config.app.executor.fcm.load_creds()?; + config.app.push_executor.fcm.load_creds()?; Ok(config) } diff --git a/core/notifications/src/cli/mod.rs b/core/notifications/src/cli/mod.rs index 5dbfcfe0d1..f0162ef11d 100644 --- a/core/notifications/src/cli/mod.rs +++ b/core/notifications/src/cli/mod.rs @@ -14,6 +14,8 @@ struct Cli { config: Option, #[clap(env = "PG_CON")] pg_con: String, + #[clap(env = "EMAIL_PASSWORD", default_value = "password")] + email_password: String, #[clap(env = "KRATOS_PG_CON")] kratos_pg_con: Option, } @@ -25,6 +27,7 @@ pub async fn run() -> anyhow::Result<()> { cli.config, EnvOverride { db_con: cli.pg_con, + email_password: cli.email_password, kratos_pg_con: cli.kratos_pg_con, }, )?; diff --git a/core/notifications/src/email_executor/config.rs b/core/notifications/src/email_executor/config.rs new file mode 100644 index 0000000000..b8c32e73bc --- /dev/null +++ b/core/notifications/src/email_executor/config.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +use super::smtp::SmtpConfig; + +#[derive(Clone, Default, Debug, Deserialize, Serialize)] +pub struct EmailExecutorConfig { + pub smtp: SmtpConfig, +} diff --git a/core/notifications/src/email_executor/error.rs b/core/notifications/src/email_executor/error.rs new file mode 100644 index 0000000000..be11e0c248 --- /dev/null +++ b/core/notifications/src/email_executor/error.rs @@ -0,0 +1,12 @@ +use thiserror::Error; + +use super::smtp::error::SmtpError; +use crate::user_notification_settings::error::*; + +#[derive(Error, Debug)] +pub enum EmailExecutorError { + #[error("EmailExecutorError - SmtpError: {0}")] + SmtpError(#[from] SmtpError), + #[error("EmailExecutorError - UserNotificationSettingsError: {0}")] + UserNotificationSettingsError(#[from] UserNotificationSettingsError), +} diff --git a/core/notifications/src/email_executor/mod.rs b/core/notifications/src/email_executor/mod.rs new file mode 100644 index 0000000000..2fd688f4b2 --- /dev/null +++ b/core/notifications/src/email_executor/mod.rs @@ -0,0 +1,49 @@ +mod config; +pub mod error; +mod smtp; + +use tracing::instrument; + +use crate::{notification_event::*, user_notification_settings::*}; + +pub use config::*; +use error::*; +use smtp::SmtpClient; + +#[derive(Clone)] +pub struct EmailExecutor { + _config: EmailExecutorConfig, + smtp: SmtpClient, + settings: UserNotificationSettingsRepo, +} + +impl EmailExecutor { + pub fn init( + config: EmailExecutorConfig, + settings: UserNotificationSettingsRepo, + ) -> Result { + let smtp = SmtpClient::init(config.smtp.clone())?; + Ok(EmailExecutor { + _config: config, + smtp, + settings, + }) + } + + #[instrument(name = "email_executor.notify", skip(self))] + pub async fn notify(&self, event: &T) -> Result<(), EmailExecutorError> { + if let Some((settings, addr)) = self + .settings + .find_for_user_id(event.user_id()) + .await + .ok() + .and_then(|s| s.email_address().map(|addr| (s, addr))) + { + let msg = event.to_localized_email(settings.locale().unwrap_or_default()); + if let Some(msg) = msg { + self.smtp.send_email(msg, addr).await?; + } + } + Ok(()) + } +} diff --git a/core/notifications/src/email_executor/smtp/config.rs b/core/notifications/src/email_executor/smtp/config.rs new file mode 100644 index 0000000000..787241a29c --- /dev/null +++ b/core/notifications/src/email_executor/smtp/config.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, Deserialize, Serialize)] +pub struct SmtpConfig { + pub username: String, + #[serde(default)] + pub password: String, + pub from_email: String, + pub relay: String, +} diff --git a/core/notifications/src/email_executor/smtp/error.rs b/core/notifications/src/email_executor/smtp/error.rs new file mode 100644 index 0000000000..5f4586b92b --- /dev/null +++ b/core/notifications/src/email_executor/smtp/error.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum SmtpError { + #[error("SmtpError - Transport : {0}")] + Transport(#[from] lettre::transport::smtp::Error), + #[error("SmtpError - Lettre : {0}")] + Lettre(#[from] lettre::error::Error), + #[error("SmtpError - Address : {0}")] + Address(#[from] lettre::address::AddressError), +} diff --git a/core/notifications/src/email_executor/smtp/mod.rs b/core/notifications/src/email_executor/smtp/mod.rs new file mode 100644 index 0000000000..8e14881c9a --- /dev/null +++ b/core/notifications/src/email_executor/smtp/mod.rs @@ -0,0 +1,47 @@ +mod config; +pub mod error; + +use lettre::{ + message::{Mailbox, Message}, + transport::smtp::authentication::Credentials, + AsyncSmtpTransport, AsyncTransport, Tokio1Executor, +}; + +use crate::{messages::LocalizedEmail, primitives::GaloyEmailAddress}; + +pub use config::*; +use error::*; + +#[derive(Clone)] +pub struct SmtpClient { + client: AsyncSmtpTransport, + from_email: String, +} + +impl SmtpClient { + pub fn init(config: SmtpConfig) -> Result { + let creds = Credentials::new(config.username, config.password); + let client: AsyncSmtpTransport = + AsyncSmtpTransport::::starttls_relay(&config.relay)? + .credentials(creds) + .build(); + Ok(Self { + client, + from_email: config.from_email, + }) + } + + pub async fn send_email( + &self, + msg: LocalizedEmail, + recipient_addr: GaloyEmailAddress, + ) -> Result<(), SmtpError> { + let email = Message::builder() + .from(Mailbox::new(None, self.from_email.parse()?)) + .to(Mailbox::new(None, recipient_addr.into_inner().parse()?)) + .subject(msg.subject) + .body(msg.body)?; + self.client.send(email).await?; + Ok(()) + } +} diff --git a/core/notifications/src/job/error.rs b/core/notifications/src/job/error.rs index c52a01a0f1..86db86603d 100644 --- a/core/notifications/src/job/error.rs +++ b/core/notifications/src/job/error.rs @@ -1,13 +1,15 @@ use thiserror::Error; -use crate::push_executor::error::PushExecutorError; +use crate::{email_executor::error::EmailExecutorError, push_executor::error::PushExecutorError}; #[derive(Error, Debug)] pub enum JobError { #[error("JobError - Sqlx: {0}")] Sqlx(#[from] sqlx::Error), - #[error("JobError - ExecutorError: {0}")] + #[error("JobError - PushExecutorError: {0}")] PushExecutor(#[from] PushExecutorError), + #[error("JobError - EmailExecutorError: {0}")] + EmailExecutor(#[from] EmailExecutorError), } impl job_executor::JobExecutionError for JobError {} diff --git a/core/notifications/src/job/mod.rs b/core/notifications/src/job/mod.rs index fa662d2963..9f0a9ccb16 100644 --- a/core/notifications/src/job/mod.rs +++ b/core/notifications/src/job/mod.rs @@ -1,3 +1,4 @@ +mod send_email_notification; mod send_push_notification; pub mod error; @@ -7,18 +8,21 @@ use tracing::instrument; use job_executor::JobExecutor; -use crate::push_executor::PushExecutor; +use crate::{email_executor::EmailExecutor, push_executor::PushExecutor}; use error::JobError; +use send_email_notification::SendEmailNotificationData; use send_push_notification::SendPushNotificationData; pub async fn start_job_runner( pool: &sqlx::PgPool, - executor: PushExecutor, + push_executor: PushExecutor, + email_executor: EmailExecutor, ) -> Result { - let mut registry = JobRegistry::new(&[send_push_notification]); - registry.set_context(executor); + let mut registry = JobRegistry::new(&[send_push_notification, send_email_notification]); + registry.set_context(push_executor); + registry.set_context(email_executor); Ok(registry.runner(pool).set_keep_alive(false).run().await?) } @@ -61,3 +65,42 @@ pub async fn spawn_send_push_notification( } Ok(()) } + +#[job( + name = "send_email_notification", + channel_name = "send_email_notification" +)] +async fn send_email_notification( + mut current_job: CurrentJob, + executor: EmailExecutor, +) -> Result<(), JobError> { + JobExecutor::builder(&mut current_job) + .build() + .expect("couldn't build JobExecutor") + .execute(|data| async move { + let data: SendEmailNotificationData = + data.expect("no SendEmailNotificationData available"); + send_email_notification::execute(data, executor).await + }) + .await?; + Ok(()) +} + +#[instrument(name = "job.spawn_send_email_notification", skip_all, fields(error, error.level, error.message), err)] +pub async fn spawn_send_email_notification( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + data: impl Into, +) -> Result<(), JobError> { + let data = data.into(); + if let Err(e) = send_email_notification + .builder() + .set_json(&data) + .expect("Couldn't set json") + .spawn(&mut **tx) + .await + { + tracing::insert_error_fields(tracing::Level::WARN, &e); + return Err(e.into()); + } + Ok(()) +} diff --git a/core/notifications/src/job/send_email_notification.rs b/core/notifications/src/job/send_email_notification.rs new file mode 100644 index 0000000000..1a9ae9e577 --- /dev/null +++ b/core/notifications/src/job/send_email_notification.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; +use tracing::instrument; + +use std::collections::HashMap; + +use super::error::JobError; +use crate::{email_executor::EmailExecutor, notification_event::NotificationEventPayload}; + +#[derive(Debug, Serialize, Deserialize)] +pub(super) struct SendEmailNotificationData { + payload: NotificationEventPayload, + #[serde(flatten)] + pub(super) tracing_data: HashMap, +} + +impl From for SendEmailNotificationData { + fn from(payload: NotificationEventPayload) -> Self { + Self { + payload, + tracing_data: tracing::extract_tracing_data(), + } + } +} + +#[instrument(name = "job.send_email_notification", skip(executor), err)] +pub async fn execute( + data: SendEmailNotificationData, + executor: EmailExecutor, +) -> Result { + executor.notify(&data.payload).await?; + Ok(data) +} diff --git a/core/notifications/src/lib.rs b/core/notifications/src/lib.rs index f662191b70..b635904ddd 100644 --- a/core/notifications/src/lib.rs +++ b/core/notifications/src/lib.rs @@ -9,6 +9,7 @@ mod job; mod messages; pub mod cli; +pub mod email_executor; pub mod graphql; pub mod grpc; pub mod notification_event; diff --git a/core/notifications/src/messages/mod.rs b/core/notifications/src/messages/mod.rs index ce581795b0..0de7ec7fa3 100644 --- a/core/notifications/src/messages/mod.rs +++ b/core/notifications/src/messages/mod.rs @@ -2,15 +2,15 @@ use rust_i18n::t; use crate::{notification_event::*, primitives::*}; -pub struct LocalizedMessage { +pub struct LocalizedPushMessage { pub title: String, pub body: String, } -pub struct Messages {} +pub struct PushMessages {} -impl Messages { - pub fn circle_grew(locale: &str, event: &CircleGrew) -> LocalizedMessage { +impl PushMessages { + pub fn circle_grew(locale: &str, event: &CircleGrew) -> LocalizedPushMessage { let circle_type = match event.circle_type { CircleType::Inner => t!("circle_type.inner", locale = locale), CircleType::Outer => t!("circle_type.outer", locale = locale), @@ -22,13 +22,13 @@ impl Messages { circle_type = circle_type ) .to_string(); - LocalizedMessage { title, body } + LocalizedPushMessage { title, body } } pub fn circle_threshold_reached( locale: &str, event: &CircleThresholdReached, - ) -> LocalizedMessage { + ) -> LocalizedPushMessage { let title = match event.circle_type { CircleType::Inner => t!("circle_threshold_reached.inner.title", locale = locale), CircleType::Outer => t!("circle_threshold_reached.outer.title", locale = locale), @@ -53,22 +53,22 @@ impl Messages { ), } .to_string(); - LocalizedMessage { title, body } + LocalizedPushMessage { title, body } } pub fn identity_verification_approved( locale: &str, _event: &IdentityVerificationApproved, - ) -> LocalizedMessage { + ) -> LocalizedPushMessage { let title = t!("identity_verification_approved.title", locale = locale).to_string(); let body = t!("identity_verification_approved.body", locale = locale).to_string(); - LocalizedMessage { title, body } + LocalizedPushMessage { title, body } } pub fn identity_verification_declined( locale: &str, event: &IdentityVerificationDeclined, - ) -> LocalizedMessage { + ) -> LocalizedPushMessage { let reason = match event.declined_reason { IdentityVerificationDeclinedReason::DocumentsNotClear => { t!( @@ -114,20 +114,61 @@ impl Messages { reason = reason ) .to_string(); - LocalizedMessage { title, body } + LocalizedPushMessage { title, body } } pub fn identity_verification_review_pending( locale: &str, _event: &IdentityVerificationReviewPending, - ) -> LocalizedMessage { + ) -> LocalizedPushMessage { let title = t!( "identity_verification_review_pending.title", locale = locale ) .to_string(); let body = t!("identity_verification_review_pending.body", locale = locale).to_string(); - LocalizedMessage { title, body } + LocalizedPushMessage { title, body } + } +} + +pub struct LocalizedEmail { + pub subject: String, + pub body: String, +} + +pub struct EmailMessages {} + +impl EmailMessages { + pub fn circle_grew(_locale: &str, _event: &CircleGrew) -> Option { + None + } + + pub fn circle_threshold_reached( + _locale: &str, + _event: &CircleThresholdReached, + ) -> Option { + None + } + + pub fn identity_verification_approved( + _locale: &str, + _event: &IdentityVerificationApproved, + ) -> Option { + None + } + + pub fn identity_verification_declined( + _locale: &str, + _event: &IdentityVerificationDeclined, + ) -> Option { + None + } + + pub fn identity_verification_review_pending( + _locale: &str, + _event: &IdentityVerificationReviewPending, + ) -> Option { + None } } @@ -143,7 +184,7 @@ mod tests { this_month_circle_size: 1, all_time_circle_size: 2, }; - let localized_message = Messages::circle_grew("en", &event); + let localized_message = PushMessages::circle_grew("en", &event); assert_eq!(localized_message.title, "Your Blink Circles are growing!"); assert_eq!( localized_message.body, @@ -159,14 +200,14 @@ mod tests { time_frame: CircleTimeFrame::AllTime, threshold: 2, }; - let localized_message = Messages::circle_threshold_reached("en", &event); + let localized_message = PushMessages::circle_threshold_reached("en", &event); assert_eq!(localized_message.title, "Nice Inner Circle! 🤙"); assert_eq!( localized_message.body, "You have welcomed 2 people to Blink. Keep it up!" ); - let localized_message = Messages::circle_threshold_reached("de", &event); + let localized_message = PushMessages::circle_threshold_reached("de", &event); assert_eq!(localized_message.title, "Schöner Innerer Kreis! 🤙"); assert_eq!( localized_message.body, diff --git a/core/notifications/src/notification_event.rs b/core/notifications/src/notification_event.rs index 2b3bb0d3e8..1b8a1bb423 100644 --- a/core/notifications/src/notification_event.rs +++ b/core/notifications/src/notification_event.rs @@ -7,14 +7,16 @@ pub enum DeepLink { Circles, } -pub trait NotificationEvent: std::fmt::Debug + Into { +pub trait NotificationEvent: std::fmt::Debug + Into + Clone { fn category(&self) -> UserNotificationCategory; fn user_id(&self) -> &GaloyUserId; fn deep_link(&self) -> DeepLink; - fn to_localized_msg(&self, locale: GaloyLocale) -> LocalizedMessage; + fn to_localized_push_msg(&self, locale: GaloyLocale) -> LocalizedPushMessage; + fn should_send_email(&self) -> bool; + fn to_localized_email(&self, locale: GaloyLocale) -> Option; } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "type", rename_all = "snake_case")] pub enum NotificationEventPayload { CircleGrew(CircleGrew), @@ -55,26 +57,60 @@ impl NotificationEvent for NotificationEventPayload { } } - fn to_localized_msg(&self, locale: GaloyLocale) -> LocalizedMessage { + fn to_localized_push_msg(&self, locale: GaloyLocale) -> LocalizedPushMessage { match self { - NotificationEventPayload::CircleGrew(event) => event.to_localized_msg(locale), + NotificationEventPayload::CircleGrew(event) => event.to_localized_push_msg(locale), NotificationEventPayload::CircleThresholdReached(event) => { - event.to_localized_msg(locale) + event.to_localized_push_msg(locale) } NotificationEventPayload::IdentityVerificationApproved(event) => { - event.to_localized_msg(locale) + event.to_localized_push_msg(locale) } NotificationEventPayload::IdentityVerificationDeclined(event) => { - event.to_localized_msg(locale) + event.to_localized_push_msg(locale) } NotificationEventPayload::IdentityVerificationReviewPending(event) => { - event.to_localized_msg(locale) + event.to_localized_push_msg(locale) + } + } + } + + fn to_localized_email(&self, locale: GaloyLocale) -> Option { + match self { + NotificationEventPayload::CircleGrew(event) => event.to_localized_email(locale), + NotificationEventPayload::CircleThresholdReached(event) => { + event.to_localized_email(locale) + } + NotificationEventPayload::IdentityVerificationApproved(event) => { + event.to_localized_email(locale) + } + NotificationEventPayload::IdentityVerificationDeclined(event) => { + event.to_localized_email(locale) + } + NotificationEventPayload::IdentityVerificationReviewPending(event) => { + event.to_localized_email(locale) + } + } + } + + fn should_send_email(&self) -> bool { + match self { + NotificationEventPayload::CircleGrew(event) => event.should_send_email(), + NotificationEventPayload::CircleThresholdReached(event) => event.should_send_email(), + NotificationEventPayload::IdentityVerificationApproved(event) => { + event.should_send_email() + } + NotificationEventPayload::IdentityVerificationDeclined(event) => { + event.should_send_email() + } + NotificationEventPayload::IdentityVerificationReviewPending(event) => { + event.should_send_email() } } } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct CircleGrew { pub user_id: GaloyUserId, pub circle_type: CircleType, @@ -95,8 +131,16 @@ impl NotificationEvent for CircleGrew { DeepLink::Circles } - fn to_localized_msg(&self, locale: GaloyLocale) -> LocalizedMessage { - Messages::circle_grew(locale.as_ref(), self) + fn to_localized_push_msg(&self, locale: GaloyLocale) -> LocalizedPushMessage { + PushMessages::circle_grew(locale.as_ref(), self) + } + + fn to_localized_email(&self, locale: GaloyLocale) -> Option { + EmailMessages::circle_grew(locale.as_ref(), self) + } + + fn should_send_email(&self) -> bool { + false } } @@ -106,7 +150,7 @@ impl From for NotificationEventPayload { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct CircleThresholdReached { pub user_id: GaloyUserId, pub circle_type: CircleType, @@ -127,8 +171,16 @@ impl NotificationEvent for CircleThresholdReached { DeepLink::Circles } - fn to_localized_msg(&self, locale: GaloyLocale) -> LocalizedMessage { - Messages::circle_threshold_reached(locale.as_ref(), self) + fn to_localized_push_msg(&self, locale: GaloyLocale) -> LocalizedPushMessage { + PushMessages::circle_threshold_reached(locale.as_ref(), self) + } + + fn to_localized_email(&self, locale: GaloyLocale) -> Option { + EmailMessages::circle_threshold_reached(locale.as_ref(), self) + } + + fn should_send_email(&self) -> bool { + false } } @@ -138,7 +190,7 @@ impl From for NotificationEventPayload { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct IdentityVerificationApproved { pub user_id: GaloyUserId, } @@ -156,8 +208,16 @@ impl NotificationEvent for IdentityVerificationApproved { DeepLink::None } - fn to_localized_msg(&self, locale: GaloyLocale) -> LocalizedMessage { - Messages::identity_verification_approved(locale.as_ref(), self) + fn to_localized_push_msg(&self, locale: GaloyLocale) -> LocalizedPushMessage { + PushMessages::identity_verification_approved(locale.as_ref(), self) + } + + fn to_localized_email(&self, locale: GaloyLocale) -> Option { + EmailMessages::identity_verification_approved(locale.as_ref(), self) + } + + fn should_send_email(&self) -> bool { + false } } @@ -167,7 +227,7 @@ impl From for NotificationEventPayload { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub enum IdentityVerificationDeclinedReason { DocumentsNotClear, SelfieNotClear, @@ -177,7 +237,7 @@ pub enum IdentityVerificationDeclinedReason { Other, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct IdentityVerificationDeclined { pub user_id: GaloyUserId, pub declined_reason: IdentityVerificationDeclinedReason, @@ -196,8 +256,16 @@ impl NotificationEvent for IdentityVerificationDeclined { DeepLink::None } - fn to_localized_msg(&self, locale: GaloyLocale) -> LocalizedMessage { - Messages::identity_verification_declined(locale.as_ref(), self) + fn to_localized_push_msg(&self, locale: GaloyLocale) -> LocalizedPushMessage { + PushMessages::identity_verification_declined(locale.as_ref(), self) + } + + fn to_localized_email(&self, locale: GaloyLocale) -> Option { + EmailMessages::identity_verification_declined(locale.as_ref(), self) + } + + fn should_send_email(&self) -> bool { + false } } @@ -207,7 +275,7 @@ impl From for NotificationEventPayload { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct IdentityVerificationReviewPending { pub user_id: GaloyUserId, } @@ -225,8 +293,16 @@ impl NotificationEvent for IdentityVerificationReviewPending { DeepLink::None } - fn to_localized_msg(&self, locale: GaloyLocale) -> LocalizedMessage { - Messages::identity_verification_review_pending(locale.as_ref(), self) + fn to_localized_push_msg(&self, locale: GaloyLocale) -> LocalizedPushMessage { + PushMessages::identity_verification_review_pending(locale.as_ref(), self) + } + + fn to_localized_email(&self, locale: GaloyLocale) -> Option { + EmailMessages::identity_verification_review_pending(locale.as_ref(), self) + } + + fn should_send_email(&self) -> bool { + false } } diff --git a/core/notifications/src/primitives.rs b/core/notifications/src/primitives.rs index 1fec7bece2..aa9ddd3521 100644 --- a/core/notifications/src/primitives.rs +++ b/core/notifications/src/primitives.rs @@ -120,7 +120,7 @@ pub enum UserNotificationCategory { AdminNotification, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub enum CircleType { Inner, Outer, @@ -135,7 +135,7 @@ impl std::fmt::Display for CircleType { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub enum CircleTimeFrame { Month, AllTime, diff --git a/core/notifications/src/push_executor/fcm/mod.rs b/core/notifications/src/push_executor/fcm/mod.rs index 66a3fe64e6..ffc448c58b 100644 --- a/core/notifications/src/push_executor/fcm/mod.rs +++ b/core/notifications/src/push_executor/fcm/mod.rs @@ -11,7 +11,7 @@ use google_fcm1::{ use std::collections::HashMap; -use crate::{messages::LocalizedMessage, notification_event::*, primitives::PushDeviceToken}; +use crate::{messages::LocalizedPushMessage, notification_event::*, primitives::PushDeviceToken}; pub use config::*; use error::*; @@ -63,7 +63,7 @@ impl FcmClient { pub async fn send( &self, device_token: &PushDeviceToken, - msg: &LocalizedMessage, + msg: &LocalizedPushMessage, deep_link: DeepLink, ) -> Result<(), FcmError> { let mut data = HashMap::new(); diff --git a/core/notifications/src/push_executor/mod.rs b/core/notifications/src/push_executor/mod.rs index 65f733afaf..208c8c7eaa 100644 --- a/core/notifications/src/push_executor/mod.rs +++ b/core/notifications/src/push_executor/mod.rs @@ -41,7 +41,7 @@ impl PushExecutor { return Ok(()); } - let msg = event.to_localized_msg(settings.locale().unwrap_or_default()); + let msg = event.to_localized_push_msg(settings.locale().unwrap_or_default()); let mut should_persist = false; let mut last_err = None; diff --git a/third-party/rust/BUCK b/third-party/rust/BUCK index f50d0f3b34..d9835caade 100644 --- a/third-party/rust/BUCK +++ b/third-party/rust/BUCK @@ -1007,6 +1007,34 @@ cargo.rust_library( ], ) +http_archive( + name = "chumsky-0.9.3.crate", + sha256 = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9", + strip_prefix = "chumsky-0.9.3", + urls = ["https://crates.io/api/v1/crates/chumsky/0.9.3/download"], + visibility = [], +) + +cargo.rust_library( + name = "chumsky-0.9.3", + srcs = [":chumsky-0.9.3.crate"], + crate = "chumsky", + crate_root = "chumsky-0.9.3.crate/src/lib.rs", + edition = "2018", + features = [ + "ahash", + "default", + "spill-stack", + "stacker", + "std", + ], + visibility = [], + deps = [ + ":hashbrown-0.14.2", + ":stacker-0.1.15", + ], +) + alias( name = "clap", actual = ":clap-4.5.1", @@ -1818,6 +1846,44 @@ cargo.rust_library( deps = [":serde-1.0.196"], ) +http_archive( + name = "email-encoding-0.2.0.crate", + sha256 = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75", + strip_prefix = "email-encoding-0.2.0", + urls = ["https://crates.io/api/v1/crates/email-encoding/0.2.0/download"], + visibility = [], +) + +cargo.rust_library( + name = "email-encoding-0.2.0", + srcs = [":email-encoding-0.2.0.crate"], + crate = "email_encoding", + crate_root = "email-encoding-0.2.0.crate/src/lib.rs", + edition = "2021", + visibility = [], + deps = [ + ":base64-0.21.5", + ":memchr-2.6.4", + ], +) + +http_archive( + name = "email_address-0.2.4.crate", + sha256 = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112", + strip_prefix = "email_address-0.2.4", + urls = ["https://crates.io/api/v1/crates/email_address/0.2.4/download"], + visibility = [], +) + +cargo.rust_library( + name = "email_address-0.2.4", + srcs = [":email_address-0.2.4.crate"], + crate = "email_address", + crate_root = "email_address-0.2.4.crate/src/lib.rs", + edition = "2018", + visibility = [], +) + http_archive( name = "encoding_rs-0.8.33.crate", sha256 = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1", @@ -3132,6 +3198,32 @@ cargo.rust_library( ], ) +http_archive( + name = "idna-0.5.0.crate", + sha256 = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6", + strip_prefix = "idna-0.5.0", + urls = ["https://crates.io/api/v1/crates/idna/0.5.0/download"], + visibility = [], +) + +cargo.rust_library( + name = "idna-0.5.0", + srcs = [":idna-0.5.0.crate"], + crate = "idna", + crate_root = "idna-0.5.0.crate/src/lib.rs", + edition = "2018", + features = [ + "alloc", + "default", + "std", + ], + visibility = [], + deps = [ + ":unicode-bidi-0.3.13", + ":unicode-normalization-0.1.22", + ], +) + http_archive( name = "ignore-0.4.22.crate", sha256 = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1", @@ -3406,6 +3498,61 @@ cargo.rust_library( deps = [":spin-0.5.2"], ) +alias( + name = "lettre", + actual = ":lettre-0.11.4", + visibility = ["PUBLIC"], +) + +http_archive( + name = "lettre-0.11.4.crate", + sha256 = "357ff5edb6d8326473a64c82cf41ddf78ab116f89668c50c4fac1b321e5e80f4", + strip_prefix = "lettre-0.11.4", + urls = ["https://crates.io/api/v1/crates/lettre/0.11.4/download"], + visibility = [], +) + +cargo.rust_library( + name = "lettre-0.11.4", + srcs = [":lettre-0.11.4.crate"], + crate = "lettre", + crate_root = "lettre-0.11.4.crate/src/lib.rs", + edition = "2021", + features = [ + "builder", + "rustls-tls", + "smtp-transport", + "tokio1", + "tokio1-rustls-tls", + ], + named_deps = { + "tokio1_crate": ":tokio-1.36.0", + "tokio1_rustls": ":tokio-rustls-0.25.0", + }, + visibility = [], + deps = [ + ":async-trait-0.1.74", + ":base64-0.21.5", + ":chumsky-0.9.3", + ":email-encoding-0.2.0", + ":email_address-0.2.4", + ":fastrand-2.0.1", + ":futures-io-0.3.30", + ":futures-util-0.3.30", + ":httpdate-1.0.3", + ":idna-0.5.0", + ":mime-0.3.17", + ":nom-7.1.3", + ":percent-encoding-2.3.0", + ":quoted_printable-0.5.0", + ":rustls-0.22.2", + ":rustls-pemfile-2.1.0", + ":socket2-0.5.5", + ":url-2.4.1", + ":webpki-roots-0.26.1", + ], +) + http_archive( name = "libc-0.2.149.crate", sha256 = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b", @@ -5210,6 +5357,23 @@ cargo.rust_library( deps = [":prost-0.12.3"], ) +http_archive( + name = "psm-0.1.21.crate", + sha256 = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874", + strip_prefix = "psm-0.1.21", + urls = ["https://crates.io/api/v1/crates/psm/0.1.21/download"], + visibility = [], +) + +cargo.rust_library( + name = "psm-0.1.21", + srcs = [":psm-0.1.21.crate"], + crate = "psm", + crate_root = "psm-0.1.21.crate/src/lib.rs", + edition = "2015", + visibility = [], +) + http_archive( name = "quote-1.0.35.crate", sha256 = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef", @@ -5232,6 +5396,27 @@ cargo.rust_library( deps = [":proc-macro2-1.0.76"], ) +http_archive( + name = "quoted_printable-0.5.0.crate", + sha256 = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0", + strip_prefix = "quoted_printable-0.5.0", + urls = ["https://crates.io/api/v1/crates/quoted_printable/0.5.0/download"], + visibility = [], +) + +cargo.rust_library( + name = "quoted_printable-0.5.0", + srcs = [":quoted_printable-0.5.0.crate"], + crate = "quoted_printable", + crate_root = "quoted_printable-0.5.0.crate/src/lib.rs", + edition = "2018", + features = [ + "default", + "std", + ], + visibility = [], +) + alias( name = "rand", actual = ":rand-0.8.5", @@ -6572,7 +6757,7 @@ cargo.rust_library( "tls12", ], named_deps = { - "pki_types": ":rustls-pki-types-1.2.0", + "pki_types": ":rustls-pki-types-1.3.0", }, visibility = [], deps = [ @@ -6641,18 +6826,43 @@ cargo.rust_library( ) http_archive( - name = "rustls-pki-types-1.2.0.crate", - sha256 = "0a716eb65e3158e90e17cd93d855216e27bde02745ab842f2cab4a39dba1bacf", - strip_prefix = "rustls-pki-types-1.2.0", - urls = ["https://crates.io/api/v1/crates/rustls-pki-types/1.2.0/download"], + name = "rustls-pemfile-2.1.0.crate", + sha256 = "3c333bb734fcdedcea57de1602543590f545f127dc8b533324318fd492c5c70b", + strip_prefix = "rustls-pemfile-2.1.0", + urls = ["https://crates.io/api/v1/crates/rustls-pemfile/2.1.0/download"], + visibility = [], +) + +cargo.rust_library( + name = "rustls-pemfile-2.1.0", + srcs = [":rustls-pemfile-2.1.0.crate"], + crate = "rustls_pemfile", + crate_root = "rustls-pemfile-2.1.0.crate/src/lib.rs", + edition = "2018", + features = [ + "default", + "std", + ], + named_deps = { + "pki_types": ":rustls-pki-types-1.3.0", + }, + visibility = [], + deps = [":base64-0.21.5"], +) + +http_archive( + name = "rustls-pki-types-1.3.0.crate", + sha256 = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7", + strip_prefix = "rustls-pki-types-1.3.0", + urls = ["https://crates.io/api/v1/crates/rustls-pki-types/1.3.0/download"], visibility = [], ) cargo.rust_library( - name = "rustls-pki-types-1.2.0", - srcs = [":rustls-pki-types-1.2.0.crate"], + name = "rustls-pki-types-1.3.0", + srcs = [":rustls-pki-types-1.3.0.crate"], crate = "rustls_pki_types", - crate_root = "rustls-pki-types-1.2.0.crate/src/lib.rs", + crate_root = "rustls-pki-types-1.3.0.crate/src/lib.rs", edition = "2021", features = [ "alloc", @@ -6708,7 +6918,7 @@ cargo.rust_library( "std", ], named_deps = { - "pki_types": ":rustls-pki-types-1.2.0", + "pki_types": ":rustls-pki-types-1.3.0", }, visibility = [], deps = [ @@ -8095,6 +8305,36 @@ cargo.rust_library( visibility = [], ) +http_archive( + name = "stacker-0.1.15.crate", + sha256 = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce", + strip_prefix = "stacker-0.1.15", + urls = ["https://crates.io/api/v1/crates/stacker/0.1.15/download"], + visibility = [], +) + +cargo.rust_library( + name = "stacker-0.1.15", + srcs = [":stacker-0.1.15.crate"], + crate = "stacker", + crate_root = "stacker-0.1.15.crate/src/lib.rs", + edition = "2015", + platform = { + "windows-gnu": dict( + deps = [":winapi-0.3.9"], + ), + "windows-msvc": dict( + deps = [":winapi-0.3.9"], + ), + }, + visibility = [], + deps = [ + ":cfg-if-1.0.0", + ":libc-0.2.149", + ":psm-0.1.21", + ], +) + http_archive( name = "static_assertions-1.1.0.crate", sha256 = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f", @@ -8422,6 +8662,7 @@ cargo.rust_binary( ":google-fcm1-5.0.3+20230106", ":http-0.2.11", ":jsonwebtoken-9.2.0", + ":lettre-0.11.4", ":opentelemetry-0.20.0", ":opentelemetry-http-0.9.0", ":opentelemetry-otlp-0.13.0", @@ -8827,6 +9068,36 @@ cargo.rust_library( ], ) +http_archive( + name = "tokio-rustls-0.25.0.crate", + sha256 = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f", + strip_prefix = "tokio-rustls-0.25.0", + urls = ["https://crates.io/api/v1/crates/tokio-rustls/0.25.0/download"], + visibility = [], +) + +cargo.rust_library( + name = "tokio-rustls-0.25.0", + srcs = [":tokio-rustls-0.25.0.crate"], + crate = "tokio_rustls", + crate_root = "tokio-rustls-0.25.0.crate/src/lib.rs", + edition = "2021", + features = [ + "default", + "logging", + "ring", + "tls12", + ], + named_deps = { + "pki_types": ":rustls-pki-types-1.3.0", + }, + visibility = [], + deps = [ + ":rustls-0.22.2", + ":tokio-1.36.0", + ], +) + http_archive( name = "tokio-stream-0.1.14.crate", sha256 = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842", @@ -10080,6 +10351,26 @@ cargo.rust_library( visibility = [], ) +http_archive( + name = "webpki-roots-0.26.1.crate", + sha256 = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009", + strip_prefix = "webpki-roots-0.26.1", + urls = ["https://crates.io/api/v1/crates/webpki-roots/0.26.1/download"], + visibility = [], +) + +cargo.rust_library( + name = "webpki-roots-0.26.1", + srcs = [":webpki-roots-0.26.1.crate"], + crate = "webpki_roots", + crate_root = "webpki-roots-0.26.1.crate/src/lib.rs", + edition = "2018", + named_deps = { + "pki_types": ":rustls-pki-types-1.3.0", + }, + visibility = [], +) + http_archive( name = "which-4.4.2.crate", sha256 = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7", @@ -10161,10 +10452,13 @@ cargo.rust_library( features = [ "consoleapi", "errhandlingapi", + "fibersapi", "fileapi", "handleapi", + "memoryapi", "minwindef", "processenv", + "processthreadsapi", "std", "sysinfoapi", "winbase", @@ -10195,10 +10489,13 @@ cargo.rust_binary( features = [ "consoleapi", "errhandlingapi", + "fibersapi", "fileapi", "handleapi", + "memoryapi", "minwindef", "processenv", + "processthreadsapi", "std", "sysinfoapi", "winbase", @@ -10218,10 +10515,13 @@ buildscript_run( features = [ "consoleapi", "errhandlingapi", + "fibersapi", "fileapi", "handleapi", + "memoryapi", "minwindef", "processenv", + "processthreadsapi", "std", "sysinfoapi", "winbase", diff --git a/third-party/rust/Cargo.lock b/third-party/rust/Cargo.lock index be9d225f95..ca715ac440 100644 --- a/third-party/rust/Cargo.lock +++ b/third-party/rust/Cargo.lock @@ -460,6 +460,16 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.2", + "stacker", +] + [[package]] name = "clap" version = "4.5.1" @@ -768,6 +778,22 @@ dependencies = [ "serde", ] +[[package]] +name = "email-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +dependencies = [ + "base64 0.21.5", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -1241,7 +1267,7 @@ dependencies = [ "rustls 0.21.8", "rustls-native-certs", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", ] [[package]] @@ -1306,6 +1332,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "ignore" version = "0.4.22" @@ -1416,6 +1452,35 @@ dependencies = [ "spin 0.5.2", ] +[[package]] +name = "lettre" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357ff5edb6d8326473a64c82cf41ddf78ab116f89668c50c4fac1b321e5e80f4" +dependencies = [ + "async-trait", + "base64 0.21.5", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "httpdate", + "idna 0.5.0", + "mime", + "nom", + "percent-encoding 2.3.0", + "quoted_printable", + "rustls 0.22.2", + "rustls-pemfile 2.1.0", + "socket2 0.5.5", + "tokio", + "tokio-rustls 0.25.0", + "url 2.4.1", + "webpki-roots 0.26.1", +] + [[package]] name = "libc" version = "0.2.149" @@ -2065,6 +2130,15 @@ dependencies = [ "prost 0.12.3", ] +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + [[package]] name = "quote" version = "1.0.35" @@ -2074,6 +2148,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0" + [[package]] name = "rand" version = "0.8.5" @@ -2181,14 +2261,14 @@ dependencies = [ "percent-encoding 2.3.0", "pin-project-lite", "rustls 0.21.8", - "rustls-pemfile", + "rustls-pemfile 1.0.3", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "system-configuration", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tower-service", "url 2.4.1", "wasm-bindgen", @@ -2335,7 +2415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 1.0.3", "schannel", "security-framework", ] @@ -2349,11 +2429,21 @@ dependencies = [ "base64 0.21.5", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c333bb734fcdedcea57de1602543590f545f127dc8b533324318fd492c5c70b" +dependencies = [ + "base64 0.21.5", + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a716eb65e3158e90e17cd93d855216e27bde02745ab842f2cab4a39dba1bacf" +checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7" [[package]] name = "rustls-webpki" @@ -2772,7 +2862,7 @@ dependencies = [ "paste", "percent-encoding 2.3.0", "rustls 0.21.8", - "rustls-pemfile", + "rustls-pemfile 1.0.3", "serde", "serde_json", "sha2", @@ -2970,6 +3060,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stacker" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "winapi", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -3104,6 +3207,7 @@ dependencies = [ "google-fcm1", "http", "jsonwebtoken", + "lettre", "opentelemetry", "opentelemetry-http", "opentelemetry-otlp", @@ -3256,6 +3360,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.2", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -3830,6 +3945,15 @@ version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +[[package]] +name = "webpki-roots" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "4.4.2" @@ -4065,7 +4189,7 @@ dependencies = [ "log", "percent-encoding 2.3.0", "rustls 0.22.2", - "rustls-pemfile", + "rustls-pemfile 1.0.3", "seahash", "serde", "serde_json", diff --git a/third-party/rust/Cargo.toml b/third-party/rust/Cargo.toml index c2158a333a..54674699f0 100644 --- a/third-party/rust/Cargo.toml +++ b/third-party/rust/Cargo.toml @@ -49,6 +49,7 @@ opentelemetry-http = "0.9.0" http = "0.2.11" rust-i18n = "3" google-fcm1 = "5.0.3" +lettre = { version = "0.11.4", default-features = false, features = ["builder", "tokio1", "tokio1-rustls-tls", "smtp-transport"] } prost = "0.12" tonic = "0.10.2" diff --git a/third-party/rust/fixups/psm/fixups.toml b/third-party/rust/fixups/psm/fixups.toml new file mode 100644 index 0000000000..db40d72cb2 --- /dev/null +++ b/third-party/rust/fixups/psm/fixups.toml @@ -0,0 +1 @@ +buildscript = [] diff --git a/third-party/rust/fixups/stacker/fixups.toml b/third-party/rust/fixups/stacker/fixups.toml new file mode 100644 index 0000000000..db40d72cb2 --- /dev/null +++ b/third-party/rust/fixups/stacker/fixups.toml @@ -0,0 +1 @@ +buildscript = []