From c75b3899271260674397b2f590f70041f4008e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 13 Feb 2026 10:21:02 +0100 Subject: [PATCH 1/7] New account email --- Cargo.lock | 12 +- .../src/enrollment_management.rs | 40 ++---- crates/defguard_mail/src/templates.rs | 57 ++++++--- crates/defguard_mail/src/tests.rs | 26 +++- .../defguard_mail/templates/new-account.mjml | 115 ++++++++++++++++++ migrations/20260209083940_[2.0.0]_mjml.up.sql | 17 ++- 6 files changed, 212 insertions(+), 55 deletions(-) create mode 100644 crates/defguard_mail/templates/new-account.mjml diff --git a/Cargo.lock b/Cargo.lock index 17483d38b4..8cc27de97c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3212,9 +3212,9 @@ checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "6cdede44f9a69cab2899a2049e2c3bd49bf911a157f6a3353d4a91c61abbce44" dependencies = [ "libc", "log", @@ -5172,9 +5172,9 @@ checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simple_asn1" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", @@ -5926,9 +5926,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.7+spec-1.1.0" +version = "1.0.8+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" +checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc" dependencies = [ "winnow", ] diff --git a/crates/defguard_core/src/enrollment_management.rs b/crates/defguard_core/src/enrollment_management.rs index ef0ec3ca3b..ef97e666e3 100644 --- a/crates/defguard_core/src/enrollment_management.rs +++ b/crates/defguard_core/src/enrollment_management.rs @@ -1,18 +1,16 @@ use defguard_common::db::{Id, models::user::User}; -use defguard_mail::{Mail, templates}; +use defguard_mail::templates::{desktop_start_mail, new_account_mail}; use reqwest::Url; use sqlx::{PgConnection, PgExecutor}; use crate::db::models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token, TokenError}; -static ENROLLMENT_START_MAIL_SUBJECT: &str = "Defguard user enrollment"; - /// Start user enrollment process /// This creates a new enrollment token valid for 24h /// and optionally sends enrollment email notification to user pub async fn start_user_enrollment( user: &mut User, - transaction: &mut PgConnection, + mut transaction: &mut PgConnection, admin: &User, email: Option, token_timeout_seconds: u64, @@ -76,24 +74,13 @@ pub async fn start_user_enrollment( let base_message_context = enrollment .get_welcome_message_context(&mut *transaction) .await?; - let result = Mail::new( + let result = new_account_mail( &email, - ENROLLMENT_START_MAIL_SUBJECT, - templates::enrollment_start_mail( - base_message_context, - enrollment_service_url, - &enrollment.id, - ) - .map_err(|err| { - debug!( - "Cannot send an email to the user {} due to the error {}.", - user.username, - err.to_string() - ); - TokenError::NotificationError(err.to_string()) - })?, + &mut transaction, + base_message_context, + enrollment_service_url, + &enrollment.id, ) - .send() .await; match result { Ok(()) => { @@ -122,7 +109,7 @@ pub async fn start_user_enrollment( /// and optionally sends email notification to user pub async fn start_desktop_configuration( user: &User, - transaction: &mut PgConnection, + mut transaction: &mut PgConnection, admin: &User, email: Option, token_timeout_seconds: u64, @@ -183,21 +170,20 @@ pub async fn start_desktop_configuration( let base_message_context = desktop_configuration .get_welcome_message_context(&mut *transaction) .await?; - let _ = templates::desktop_start_mail( + let result = desktop_start_mail( &email, - &mut *transaction, + &mut transaction, base_message_context, &enrollment_service_url, &desktop_configuration.id, ) - .await - .map_err(|err| { + .await; + if let Err(err) = result { debug!( "Cannot send an email to the user {} due to the error {err}.", user.username, ); - TokenError::NotificationError(err.to_string()) - }); + } } } info!( diff --git a/crates/defguard_mail/src/templates.rs b/crates/defguard_mail/src/templates.rs index 9dd5315a83..511a9b30bb 100644 --- a/crates/defguard_mail/src/templates.rs +++ b/crates/defguard_mail/src/templates.rs @@ -39,10 +39,14 @@ static MFA_CODE_SUBJECT: &str = "Defguard: Multi-Factor Authentication code for static MFA_CODE_MJML: &str = include_str!("../templates/mfa-code.mjml"); // static MFA_CODE_TEXT: &str = include_str!("../templates/mfa-code.text"); +// This used to be called "enrollment-start". +static NEW_ACCOUNT_SUBJECT: &str = "Defguard user enrollment"; +static NEW_ACCOUNT_MJML: &str = include_str!("../templates/new-account.mjml"); +// static NEW_ACCOUNT_TEXT: &str = include_str!("../templates/new-account.text"); + static MAIL_BASE: &str = include_str!("../templates/base.tera"); static MAIL_MACROS: &str = include_str!("../templates/macros.tera"); static MAIL_TEST: &str = include_str!("../templates/mail_test.mjml"); -static MAIL_ENROLLMENT_START: &str = include_str!("../templates/mail_enrollment_start.tera"); static MAIL_ENROLLMENT_WELCOME: &str = include_str!("../templates/mail_enrollment_welcome.tera"); static MAIL_ENROLLMENT_ADMIN_NOTIFICATION: &str = include_str!("../templates/mail_enrollment_admin_notification.tera"); @@ -202,30 +206,45 @@ pub fn test_mail(session: Option<&SessionContext>) -> Result Result { +) -> Result<(), TemplateError> { debug!("Render an enrollment start mail template for the user."); - let (mut tera, mut context) = get_base_tera(context, None, None, None)?; + let (mut tera, mut context) = get_base_tera_mjml(context, None, None, None)?; + + let template = "new-account"; + tera.add_raw_template(template, NEW_ACCOUNT_MJML)?; + let db_context = MailContext::all_for_template(transaction, template, DEFAULT_LANG) + .await + .unwrap(); + for c in db_context { + context.insert(c.section, &c.text); + } // add required context - context.insert("enrollment_url", &enrollment_service_url); context.insert("defguard_url", &Settings::url()?); + context.insert("url", &enrollment_service_url); context.insert("token", enrollment_token); // prepare enrollment service URL enrollment_service_url .query_pairs_mut() .append_pair("token", enrollment_token); - context.insert("link_url", &enrollment_service_url); - tera.add_raw_template("mail_enrollment_start", MAIL_ENROLLMENT_START)?; + // TODO: Move to Mail once every message is converted to MJML. + let processed = tera.render(template, &context)?; + let parsed = mrml::parse(processed)?; + let opts = mrml::prelude::render::RenderOptions::default(); + let html = parsed.element.render(&opts)?; + + Mail::new(to, NEW_ACCOUNT_SUBJECT, html).send_and_forget(); - let processed = tera.render("mail_enrollment_start", &context)?; - Ok(processed) + Ok(()) } // Mail with link to enrollment service. @@ -567,16 +586,16 @@ mod test { assert_ok!(test_mail(None)); } - #[sqlx::test] - async fn test_enrollment_start_mail(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - init_config(&pool).await; - assert_ok!(enrollment_start_mail( - Context::new(), - Url::parse("http://localhost:8080").unwrap(), - "test_token" - )); - } + // #[sqlx::test] + // async fn test_enrollment_start_mail(_: PgPoolOptions, options: PgConnectOptions) { + // let pool = setup_pool(options).await; + // init_config(&pool).await; + // assert_ok!(enrollment_start_mail( + // Context::new(), + // Url::parse("http://localhost:8080").unwrap(), + // "test_token" + // )); + // } #[sqlx::test] async fn test_enrollment_welcome_mail(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_mail/src/tests.rs b/crates/defguard_mail/src/tests.rs index 1ae8d1affa..79f70b0391 100644 --- a/crates/defguard_mail/src/tests.rs +++ b/crates/defguard_mail/src/tests.rs @@ -19,7 +19,7 @@ use sqlx::{ use tera::Context; use super::templates::{ - TemplateLocation, desktop_start_mail, mfa_code_mail, new_device_added_mail, + TemplateLocation, desktop_start_mail, mfa_code_mail, new_account_mail, new_device_added_mail, }; /// Set SMTP settings from environment variables. @@ -120,3 +120,27 @@ fn send_mfa_code(_: PgPoolOptions, options: PgConnectOptions) { // Delay, so send_and_forget() can process the message. tokio::time::sleep(Duration::from_secs(2)).await; } + +#[ignore] +#[sqlx::test] +fn send_new_account(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + set_smtp_settings(&pool).await; + + let mut transaction = pool.begin().await.unwrap(); + let url = Url::parse("http://localhost:8000").unwrap(); + let context = Context::new(); + let token = "zXc6N1ndXpWFeyBuogiFp1bD1UomAbZc"; + new_account_mail( + &env::var("SMTP_TO").unwrap(), + &mut transaction, + context, + url, + token, + ) + .await + .unwrap(); + + // Delay, so send_and_forget() can process the message. + tokio::time::sleep(Duration::from_secs(2)).await; +} diff --git a/crates/defguard_mail/templates/new-account.mjml b/crates/defguard_mail/templates/new-account.mjml new file mode 100644 index 0000000000..3fb2b74cfe --- /dev/null +++ b/crates/defguard_mail/templates/new-account.mjml @@ -0,0 +1,115 @@ +{% import "macros.mjml" as macros %} +{% extends "base.mjml" %} +{% block content %} + +{{ macros::email_header() }} + + + + + + + + + + + + Desktop client + + + {{ download }} + + {{ macros::action_link(text="https://defguard.net/download/", href="https://defguard.net/download/", mj_class="s-3xl") }} + + {{ after_install }} + + + + + {{ label_url }} + + + {{ url }} + + + + + {{ label_token }} + + + {{ token }} + + + + + {{ token_info }} + + + {{ label_enroll }} + + + + + + + + + + + + + + + + + + + {{ label_mobile }} + + + {{ scan_qr }} + + + + {{ mobile_install }} + + + + + + + +
+ Android store + + + {{ download_google }} + +
+
+ + + + + + + + + +
+ + + + {{ download_apple }} + +
+
+
+
+
+
+ + + + +{% endblock content %} diff --git a/migrations/20260209083940_[2.0.0]_mjml.up.sql b/migrations/20260209083940_[2.0.0]_mjml.up.sql index ca8b9b5053..9242eaf940 100644 --- a/migrations/20260209083940_[2.0.0]_mjml.up.sql +++ b/migrations/20260209083940_[2.0.0]_mjml.up.sql @@ -6,13 +6,26 @@ CREATE TABLE mail_context ( CONSTRAINT template_section_language UNIQUE (template, section, language_tag) ); INSERT INTO mail_context (template, section, language_tag, text) VALUES - ('desktop-start', 'title', 'en_US', 'You are receiving this email to configure a new desktop client.'), + ('desktop-start', 'title', 'en_US', 'You''re receiving this email to configure a new desktop client.'), ('desktop-start', 'subtitle', 'en_US', 'Please paste this URL and token in your desktop client:'), ('desktop-start', 'label_url', 'en_US', 'URL'), ('desktop-start', 'label_token', 'en_US', 'Token'), ('desktop-start', 'configure', 'en_US', 'Configure your desktop client'), ('desktop-start', 'click', 'en_US', 'Click the button or use link below'), - ('new-device', 'title', 'en_US', 'A new device has been add to your account:'), + ('new-account', 'title', 'en_US', 'New account has been created for you.'), + ('new-account', 'subtitle', 'en_US', 'To start the enrollment process please use credentials below.'), + ('new-account', 'download', 'en_US', 'Download the official Defguard desktop client for your system.'), + ('new-account', 'after_install', 'en_US', 'After installation, please add a Defguard instance by entering:'), + ('new-account', 'label_url', 'en_US', 'URL'), + ('new-account', 'label_token', 'en_US', 'Token'), + ('new-account', 'token_info', 'en_US', 'The token is valid for 24 hours. Once the enrollment process starts, you have 10 minutes to complete it.'), + ('new-account', 'label_enroll', 'en_US', 'Enroll with desktop client'), + ('new-account', 'label_mobile', 'en_US', 'Mobile application'), + ('new-account', 'scan_qr', 'en_US', 'Scan QR code bellow to activate Defguard mobile application.'), + ('new-account', 'mobile_install', 'en_US', 'If you haven''t installed the mobile app, click one of the buttons bellow.'), + ('new-account', 'download_google', 'en_US', 'Download from Google Play'), + ('new-account', 'download_apple', 'en_US', 'Download from Apple Store'), + ('new-device', 'title', 'en_US', 'A new device has been added to your account:'), ('new-device', 'label_device', 'en_US', 'Device name'), ('new-device', 'label_pubkey', 'en_US', 'Public key'), ('mfa-code', 'title', 'en_US', 'Hello,'), From 2538a039425b5fac9808870ec55ad9cb0d1f72b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 13 Feb 2026 13:08:14 +0100 Subject: [PATCH 2/7] Introduce MailMessage --- Cargo.lock | 8 +- crates/defguard_mail/src/mail.rs | 127 +++++++++++++++++- crates/defguard_mail/src/templates.rs | 109 +++------------ .../templates/mail_enrollment_start.tera | 46 ------- .../templates/{mail_test.mjml => test.mjml} | 0 5 files changed, 151 insertions(+), 139 deletions(-) delete mode 100644 crates/defguard_mail/templates/mail_enrollment_start.tera rename crates/defguard_mail/templates/{mail_test.mjml => test.mjml} (100%) diff --git a/Cargo.lock b/Cargo.lock index 8cc27de97c..ae72f53433 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -618,9 +618,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -2962,9 +2962,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.181" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libgit2-sys" diff --git a/crates/defguard_mail/src/mail.rs b/crates/defguard_mail/src/mail.rs index 8cbc00fc10..61a1f291ce 100644 --- a/crates/defguard_mail/src/mail.rs +++ b/crates/defguard_mail/src/mail.rs @@ -7,10 +7,16 @@ use lettre::{ transport::smtp::authentication::Credentials, }; use serde::Serialize; -use tera::Context; +use sqlx::PgConnection; +use tera::{Context, Tera}; use thiserror::Error; use tracing::{debug, error, info, warn}; +use crate::{ + mail_context::MailContext, + templates::{DEFAULT_LANG, TemplateError}, +}; + use super::SmtpSettings; const SMTP_TIMEOUT: Duration = Duration::from_secs(15); @@ -257,3 +263,122 @@ impl Mail { Ok(builder.build()) } } + +/// Email messages. +pub enum MailMessage { + /// Test email to check if SMTP configuration works correctly. + Test, + Welcome, + /// Information for Defguard support. + Support, + DesktopStart, + /// Information after starting an enrollment. + NewAccount, + NewDevice, + NewDeviceLogin, + NewDeviceOCIDLogin, + /// Gateway has disconnected. + GatewayDisconnect, + /// Gateway has reconnected. + GatewayReconnect, + MFAActivation, + MFAConfigured, + /// MFA code. + MFACode, + PasswordReset, + PasswordResetDone, +} + +impl MailMessage { + /// Email subject. + pub(crate) const fn subject(&self) -> &'static str { + match self { + Self::Test => "Test message", + Self::Welcome => "Welcome message after enrollment", + Self::Support => "Support data", + Self::DesktopStart => "Defguard: Desktop client configuration", + Self::NewAccount => "Defguard: User enrollment", + Self::NewDevice => "Defguard: new device added to your account", + Self::NewDeviceLogin => "New device logged in to your account", + Self::NewDeviceOCIDLogin => "New login to OCID application", + Self::GatewayDisconnect => "Gateway disconnected", + Self::GatewayReconnect => "Gateway reconnected", + Self::MFAActivation => "Multi-Factor Authentication activation", + Self::MFAConfigured => "Multi-Factor Authentication {method} has been activated", + Self::MFACode => "Defguard: Multi-Factor Authentication code for login", + Self::PasswordReset => "Password reset", + Self::PasswordResetDone => "Password reset success", + } + } + + pub(crate) const fn template_name(&self) -> &str { + match self { + Self::Test => "test", + Self::Welcome => "welcome", + Self::Support => "support", + Self::DesktopStart => "desktop-start", + Self::NewAccount => "new-account", + Self::NewDevice => "new-device", + Self::NewDeviceLogin => "new-device-loin", + Self::NewDeviceOCIDLogin => "new-device-login-ocid", + Self::GatewayDisconnect => "gateway-disconnect", + Self::GatewayReconnect => "gateway-reconnect", + Self::MFAActivation => "mfa-activation", + Self::MFAConfigured => "mfa-configure", + Self::MFACode => "mfa-code", + Self::PasswordReset => "password-reset", + Self::PasswordResetDone => "password-reset-done", + } + } + + pub(crate) const fn mjml_template(&self) -> &str { + match self { + Self::Test => "", + Self::Welcome => "", + Self::Support => "", + Self::DesktopStart => include_str!("../templates/desktop-start.mjml"), + Self::NewAccount => include_str!("../templates/new-account.mjml"), + Self::NewDevice => include_str!("../templates/new-device.mjml"), + Self::NewDeviceLogin => "", + Self::NewDeviceOCIDLogin => "", + Self::GatewayDisconnect => "", + Self::GatewayReconnect => "", + Self::MFAActivation => "", + Self::MFAConfigured => "", + Self::MFACode => include_str!("../templates/mfa-code.mjml"), + Self::PasswordReset => "", + Self::PasswordResetDone => "", + } + } + + /// Fill `Context` from database. + pub(crate) async fn fill_context( + &self, + conn: &mut PgConnection, + context: &mut Context, + ) -> Result<(), sqlx::Error> { + let db_context = + MailContext::all_for_template(conn, self.template_name(), DEFAULT_LANG).await?; + for row in db_context { + context.insert(row.section, &row.text); + } + + Ok(()) + } + + /// Build `Mail`. + pub(crate) fn mail( + &self, + tera: &mut Tera, + context: &Context, + to: &str, + ) -> Result { + tera.add_raw_template(self.template_name(), self.mjml_template())?; + let processed = tera.render(self.template_name(), context)?; + let parsed = mrml::parse(processed)?; + let opts = mrml::prelude::render::RenderOptions::default(); + let html = parsed.element.render(&opts)?; + + Ok(Mail::new(to, self.subject(), html)) + } +} diff --git a/crates/defguard_mail/src/templates.rs b/crates/defguard_mail/src/templates.rs index 511a9b30bb..1a54cd8a54 100644 --- a/crates/defguard_mail/src/templates.rs +++ b/crates/defguard_mail/src/templates.rs @@ -20,33 +20,16 @@ use tera::{Context, Function, Tera}; use thiserror::Error; use tracing::debug; -use crate::{Mail, mail_context::MailContext}; +use crate::mail::MailMessage; -const DEFAULT_LANG: &str = "en_US"; +pub(crate) const DEFAULT_LANG: &str = "en_US"; static BASE_MJML: &str = include_str!("../templates/base.mjml"); static MACROS_MJML: &str = include_str!("../templates/macros.mjml"); -static DESKTOP_START_SUBJECT: &str = "Defguard desktop client configuration"; -static DESKTOP_START_MJML: &str = include_str!("../templates/desktop-start.mjml"); -// static DESKTOP_START_TEXT: &str = include_str!("../templates/desktop-start.text"); - -static NEW_DEVICE_SUBJECT: &str = "Defguard: new device added to your account"; -static NEW_DEVICE_MJML: &str = include_str!("../templates/new-device.mjml"); -// static NEW_DEVICE_TEXT: &str = include_str!("../templates/new-device.text"); - -static MFA_CODE_SUBJECT: &str = "Defguard: Multi-Factor Authentication code for login"; -static MFA_CODE_MJML: &str = include_str!("../templates/mfa-code.mjml"); -// static MFA_CODE_TEXT: &str = include_str!("../templates/mfa-code.text"); - -// This used to be called "enrollment-start". -static NEW_ACCOUNT_SUBJECT: &str = "Defguard user enrollment"; -static NEW_ACCOUNT_MJML: &str = include_str!("../templates/new-account.mjml"); -// static NEW_ACCOUNT_TEXT: &str = include_str!("../templates/new-account.text"); - static MAIL_BASE: &str = include_str!("../templates/base.tera"); static MAIL_MACROS: &str = include_str!("../templates/macros.tera"); -static MAIL_TEST: &str = include_str!("../templates/mail_test.mjml"); +static MAIL_TEST: &str = include_str!("../templates/test.mjml"); static MAIL_ENROLLMENT_WELCOME: &str = include_str!("../templates/mail_enrollment_welcome.tera"); static MAIL_ENROLLMENT_ADMIN_NOTIFICATION: &str = include_str!("../templates/mail_enrollment_admin_notification.tera"); @@ -71,6 +54,8 @@ pub enum TemplateError { #[error("Failed to generate email MFA code")] MfaError, #[error(transparent)] + DatabaseError(#[from] sqlx::Error), + #[error(transparent)] TemplateError(#[from] tera::Error), #[error(transparent)] UrlParseError(#[from] UrlParseError), @@ -208,7 +193,7 @@ pub fn test_mail(session: Option<&SessionContext>) -> Result, @@ -481,22 +427,9 @@ pub async fn mfa_code_mail( &Utc::now().format(MAIL_DATETIME_FORMAT).to_string(), ); - let template = "mfa-code"; - tera.add_raw_template(template, MFA_CODE_MJML)?; - let db_context = MailContext::all_for_template(transaction, template, DEFAULT_LANG) - .await - .unwrap(); - for c in db_context { - context.insert(c.section, &c.text); - } - - // TODO: Move to Mail once every message is converted to MJML. - let processed = tera.render(template, &context)?; - let parsed = mrml::parse(processed)?; - let opts = mrml::prelude::render::RenderOptions::default(); - let html = parsed.element.render(&opts)?; - - Mail::new(to, MFA_CODE_SUBJECT, html).send_and_forget(); + let message = MailMessage::MFACode; + message.fill_context(conn, &mut context).await?; + message.mail(&mut tera, &context, to)?.send_and_forget(); Ok(()) } diff --git a/crates/defguard_mail/templates/mail_enrollment_start.tera b/crates/defguard_mail/templates/mail_enrollment_start.tera deleted file mode 100644 index 289cbf9a6e..0000000000 --- a/crates/defguard_mail/templates/mail_enrollment_start.tera +++ /dev/null @@ -1,46 +0,0 @@ -{# Requires context -enrollment_url -> URL of the enrollment service -link_url -> URL of the enrollment service with the token query param included -defguard_url -> URL of defguard core Web UI -token -> enrollment token -#} -{% extends "base" %} -{% import "macros" as macros %} -{% block mail_content %} -{% set client_docs_url="https://docs.defguard.net/help/desktop-client" %} -{% set client_docs_link=macros::link(content=client_docs_url, href=client_docs_url) %} -{% set release_url="https://defguard.net/download/" %} -{% set release_link=macros::link(content=release_url, href=release_url) %} -{# intro #} -{% set section_content = [ -macros::paragraph(content="You're receiving this email because a new account has been created for you."), -macros::paragraph(content="In order to start the enrollment process please choose one of the following options:"), -] %} -{{ macros::text_section(content_array=section_content)}} -{# desktop client enrollment #} -{% set enrollment_link=macros::link(content=enrollment_url, href=enrollment_url) %} -{% set section_content = [ -macros::paragraph(content="1. Enrollment by desktop client"), -macros::paragraph(content="Download the official defguard desktop client for Windows, macOS or Linux: " ~ release_link), -macros::paragraph(content="After installation, please add a defguard instance by entering:"), -macros::paragraph(content="
  • Instance URL: " ~ enrollment_link ~ "
  • Enrollment token: " ~ token ~ "
"), -macros::paragraph(content="Please note that: the token is only valid for 24 hours after receiving this email. When the enrollment process starts user will have 10 minutes to complete the process."), -macros::paragraph(content="For more details go to the desktop client documentation: " ~ client_docs_link), -] %} -{{ macros::text_section(content_array=section_content)}} -{# web enrollment #} -{% set defguard_link=macros::link(content=defguard_url, href=defguard_url) %} -{% set section_content = [ -macros::paragraph(content="2. Enrollment via Web Browser"), -macros::paragraph(content="If you choose this option, you will be able to change your account details, set a password, and only configure a standard WireGuard client device - but not the official defguard desktop client. -Desktop client can still be activated later, by accessing your profile in defguard: " ~ defguard_link ~ "."), -macros::paragraph(content= "If you wish to do enrollment via Web, please copy & paste the following URL in your browser: "), -macros::link(content=link_url, href=link_url), -macros::paragraph(content="Please note that: this option is only valid for 24 hours after receiving this email. When the enrollment process starts user will have 10 minutes to complete the process."), -macros::paragraph(content="You can also click the buttons below to start the enrollment on website or within desktop client:"), -macros::button_link(href=link_url, text="Start enrollment"), -macros::spacer(height="20px"), -macros::button_link(href="defguard://addinstance?token=" ~ token ~ "&url=" ~ enrollment_url, text="Enroll with desktop client"), -] %} -{{ macros::text_section(content_array=section_content)}} -{% endblock %} diff --git a/crates/defguard_mail/templates/mail_test.mjml b/crates/defguard_mail/templates/test.mjml similarity index 100% rename from crates/defguard_mail/templates/mail_test.mjml rename to crates/defguard_mail/templates/test.mjml From 5e23dee5dee1753cdbf937ee4b3f625bab5ba739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 13 Feb 2026 14:21:40 +0100 Subject: [PATCH 3/7] Handle additional template images --- .../{android_store.png => google_play.png} | Bin crates/defguard_mail/assets/new_account_1.png | Bin 0 -> 10870 bytes crates/defguard_mail/assets/new_account_2.png | Bin 0 -> 14654 bytes crates/defguard_mail/src/mail.rs | 137 ++++++++++-------- crates/defguard_mail/src/templates.rs | 4 +- .../defguard_mail/templates/new-account.mjml | 6 +- 6 files changed, 85 insertions(+), 62 deletions(-) rename crates/defguard_mail/assets/{android_store.png => google_play.png} (100%) create mode 100644 crates/defguard_mail/assets/new_account_1.png create mode 100644 crates/defguard_mail/assets/new_account_2.png diff --git a/crates/defguard_mail/assets/android_store.png b/crates/defguard_mail/assets/google_play.png similarity index 100% rename from crates/defguard_mail/assets/android_store.png rename to crates/defguard_mail/assets/google_play.png diff --git a/crates/defguard_mail/assets/new_account_1.png b/crates/defguard_mail/assets/new_account_1.png new file mode 100644 index 0000000000000000000000000000000000000000..49a9e19df1f6266cfdc77c7f68fd5e9dbc9a031a GIT binary patch literal 10870 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 AUH||9 literal 0 HcmV?d00001 diff --git a/crates/defguard_mail/src/mail.rs b/crates/defguard_mail/src/mail.rs index 61a1f291ce..fac3c7a176 100644 --- a/crates/defguard_mail/src/mail.rs +++ b/crates/defguard_mail/src/mail.rs @@ -19,12 +19,41 @@ use crate::{ use super::SmtpSettings; +#[derive(Debug)] +pub struct Attachment { + filename: String, + content: Vec, +} + +impl Attachment { + /// Create new [`Attachement`]. + #[must_use] + pub fn new(filename: String, content: Vec) -> Self { + Self { filename, content } + } +} + +impl From for SinglePart { + fn from(attachment: Attachment) -> Self { + lettre::message::Attachment::new(attachment.filename) + .body(attachment.content, ContentType::TEXT_PLAIN) + } +} + const SMTP_TIMEOUT: Duration = Duration::from_secs(15); // Template images. static DEFGUARD_LOGO: &[u8] = include_bytes!("../assets/defguard.png"); static GITHUB_LOGO: &[u8] = include_bytes!("../assets/github.png"); static MASTODON_LOGO: &[u8] = include_bytes!("../assets/mastodon.png"); static X_LOGO: &[u8] = include_bytes!("../assets/x.png"); +// MFA code +static DATE_ICON: &[u8] = include_bytes!("../assets/date.png"); +static OTP_ICON: &[u8] = include_bytes!("../assets/otp.png"); +// New account +static NEW_ACCOUNT_1: &[u8] = include_bytes!("../assets/new_account_1.png"); +static NEW_ACCOUNT_2: &[u8] = include_bytes!("../assets/new_account_2.png"); +static GOOGLE_PLAY: &[u8] = include_bytes!("../assets/google_play.png"); +static APPLE: &[u8] = include_bytes!("../assets/apple.png"); #[derive(Debug, Error)] pub enum MailError { @@ -53,7 +82,8 @@ pub struct Mail { pub(crate) subject: String, content: String, context: Context, - attachments: Vec, + attachments: Vec, // text/plain + images: Vec<(String, Vec)>, // image/png } impl Mail { @@ -64,12 +94,21 @@ impl Mail { T: Into, S: Into, { + // Append images used in all templates. + let images = vec![ + (String::from("defguard"), Vec::from(DEFGUARD_LOGO)), + (String::from("github"), Vec::from(GITHUB_LOGO)), + (String::from("mastodon"), Vec::from(MASTODON_LOGO)), + (String::from("x"), Vec::from(X_LOGO)), + ]; + Self { to: to.into(), subject: subject.into(), content, context: Context::new(), attachments: Vec::new(), + images, } } @@ -106,35 +145,14 @@ impl Mail { self.attachments = attachments; self } -} - -#[derive(Debug)] -pub struct Attachment { - filename: String, - content: Vec, - content_type: ContentType, -} - -impl Attachment { - /// Create new [`Attachement`]. - #[must_use] - pub fn new(filename: String, content: Vec) -> Self { - Self { - filename, - content, - content_type: ContentType::TEXT_PLAIN, - } - } -} -impl From for SinglePart { - fn from(attachment: Attachment) -> Self { - lettre::message::Attachment::new(attachment.filename) - .body(attachment.content, attachment.content_type) + pub fn add_png_image(&mut self, name: S, bytes: &[u8]) + where + S: Into, + { + self.images.push((name.into(), Vec::from(bytes))); } -} -impl Mail { /// Converts Mail to lettre Message. /// Message structure should look like this: /// - multipart mixed @@ -154,25 +172,13 @@ impl Mail { let plain = SinglePart::plain("PLAIN IS NOT AVAILABLE AT THE MOMENT.".to_string()); let html = SinglePart::html(self.content); let image_png = "image/png".parse::().unwrap(); - let related = MultiPart::related() - .singlepart(html) - .singlepart( - lettre::message::Attachment::new_inline(String::from("defguard")) - .body(Body::new(Vec::from(DEFGUARD_LOGO)), image_png.clone()), - ) - .singlepart( - lettre::message::Attachment::new_inline(String::from("github")) - .body(Body::new(Vec::from(GITHUB_LOGO)), image_png.clone()), - ) - .singlepart( - lettre::message::Attachment::new_inline(String::from("mastodon")) - .body(Body::new(Vec::from(MASTODON_LOGO)), image_png.clone()), - ) - .singlepart( - lettre::message::Attachment::new_inline(String::from("x")) - .body(Body::new(Vec::from(X_LOGO)), image_png), + let mut related = MultiPart::related().singlepart(html); + for (name, bytes) in self.images { + related = related.singlepart( + lettre::message::Attachment::new_inline(name) + .body(Body::new(bytes), image_png.clone()), ); - + } let alternative = MultiPart::alternative() .singlepart(plain) .multipart(related); @@ -333,21 +339,22 @@ impl MailMessage { pub(crate) const fn mjml_template(&self) -> &str { match self { - Self::Test => "", - Self::Welcome => "", - Self::Support => "", + // Self::Test => "", + // Self::Welcome => "", + // Self::Support => "", Self::DesktopStart => include_str!("../templates/desktop-start.mjml"), Self::NewAccount => include_str!("../templates/new-account.mjml"), Self::NewDevice => include_str!("../templates/new-device.mjml"), - Self::NewDeviceLogin => "", - Self::NewDeviceOCIDLogin => "", - Self::GatewayDisconnect => "", - Self::GatewayReconnect => "", - Self::MFAActivation => "", - Self::MFAConfigured => "", + // Self::NewDeviceLogin => "", + // Self::NewDeviceOCIDLogin => "", + // Self::GatewayDisconnect => "", + // Self::GatewayReconnect => "", + // Self::MFAActivation => "", + // Self::MFAConfigured => "", Self::MFACode => include_str!("../templates/mfa-code.mjml"), - Self::PasswordReset => "", - Self::PasswordResetDone => "", + // Self::PasswordReset => "", + // Self::PasswordResetDone => "", + _ => "", } } @@ -379,6 +386,22 @@ impl MailMessage { let opts = mrml::prelude::render::RenderOptions::default(); let html = parsed.element.render(&opts)?; - Ok(Mail::new(to, self.subject(), html)) + let mut mail = Mail::new(to, self.subject(), html); + // Add PNG images. + match self { + Self::NewAccount => { + mail.add_png_image("new_account_1", NEW_ACCOUNT_1); + mail.add_png_image("new_account_2", NEW_ACCOUNT_2); + mail.add_png_image("google_play", GOOGLE_PLAY); + mail.add_png_image("apple", APPLE); + } + Self::MFACode => { + mail.add_png_image("date", DATE_ICON); + mail.add_png_image("otp", OTP_ICON); + } + _ => (), + } + + Ok(mail) } } diff --git a/crates/defguard_mail/src/templates.rs b/crates/defguard_mail/src/templates.rs index 1a54cd8a54..9b779376f7 100644 --- a/crates/defguard_mail/src/templates.rs +++ b/crates/defguard_mail/src/templates.rs @@ -193,7 +193,7 @@ pub fn test_mail(session: Option<&SessionContext>) -> Result - + @@ -77,7 +77,7 @@
- Android store + Android store @@ -94,7 +94,7 @@
- + From af82e429a7b6b37af8bfb2f5531afa3911cb264d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 16 Feb 2026 10:00:00 +0100 Subject: [PATCH 4/7] Build QR --- Cargo.lock | 720 +++++++++++++++--- crates/defguard_mail/Cargo.toml | 2 + crates/defguard_mail/src/lib.rs | 1 + crates/defguard_mail/src/mail.rs | 8 +- crates/defguard_mail/src/qr.rs | 28 + .../defguard_mail/templates/new-account.mjml | 2 +- 6 files changed, 664 insertions(+), 97 deletions(-) create mode 100644 crates/defguard_mail/src/qr.rs diff --git a/Cargo.lock b/Cargo.lock index ae72f53433..2dc4e81954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -176,6 +194,17 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "argon2" version = "0.5.3" @@ -189,6 +218,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "asn1-rs" version = "0.6.2" @@ -310,6 +354,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + [[package]] name = "aws-lc-rs" version = "1.15.4" @@ -463,6 +550,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitfields" version = "0.12.4" @@ -492,13 +585,22 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ "serde_core", ] +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -576,18 +678,36 @@ dependencies = [ "memchr", ] +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -771,6 +891,12 @@ dependencies = [ "cc", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.4" @@ -888,6 +1014,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1354,9 +1489,11 @@ dependencies = [ "claims", "defguard_common", "humantime", + "image", "lettre", "mrml", "pulldown-cmark", + "qrforge", "reqwest", "serde", "serde_json", @@ -1640,7 +1777,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", ] @@ -1857,6 +1994,26 @@ dependencies = [ "syn", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1895,12 +2052,56 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.1" @@ -2022,9 +2223,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -2037,9 +2238,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -2047,15 +2248,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -2075,15 +2276,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -2092,21 +2293,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -2116,7 +2317,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -2190,13 +2390,23 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "git2" version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "libgit2-sys", "log", @@ -2222,7 +2432,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "ignore", "walkdir", ] @@ -2715,6 +2925,46 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core 0.5.1", + "zune-jpeg 0.5.12", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "indexmap" version = "1.9.3" @@ -2747,6 +2997,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2829,7 +3090,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba781c43eb46c3bbf5bfda541139eed9a52b78d7c3aa74d516918885ecd63c40" dependencies = [ "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.11.0", "num-bigint", "serde", "serde_json", @@ -2877,9 +3138,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ "cpufeatures", ] @@ -2932,6 +3193,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "lettre" version = "0.11.19" @@ -2966,6 +3233,16 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libgit2-sys" version = "0.18.3+1.9.2" @@ -2990,7 +3267,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "redox_syscall 0.7.1", ] @@ -3050,6 +3327,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -3111,6 +3397,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "md-5" version = "0.10.6" @@ -3187,6 +3483,16 @@ dependencies = [ "syn", ] +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "mrml" version = "5.1.0" @@ -3212,17 +3518,17 @@ checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "native-tls" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cdede44f9a69cab2899a2049e2c3bd49bf911a157f6a3353d4a91c61abbce44" +checksum = "9d5d26952a508f321b4d3d2e80e78fc2603eaefcdf0c30783867f19586518bdc" dependencies = [ "libc", "log", "openssl", - "openssl-probe 0.1.6", + "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework", "security-framework-sys", "tempfile", ] @@ -3239,7 +3545,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -3270,6 +3576,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -3343,6 +3655,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3419,7 +3742,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-foundation", ] @@ -3440,7 +3763,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", "objc2", ] @@ -3451,7 +3774,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", "objc2", "objc2-core-foundation", @@ -3484,7 +3807,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3502,7 +3825,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "libc", "objc2", @@ -3515,7 +3838,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", ] @@ -3526,7 +3849,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3538,7 +3861,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "objc2", "objc2-cloud-kit", @@ -3657,7 +3980,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "foreign-types", "libc", @@ -3677,12 +4000,6 @@ dependencies = [ "syn", ] -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -4104,6 +4421,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polyval" version = "0.6.2" @@ -4183,6 +4513,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "prost" version = "0.14.3" @@ -4268,7 +4617,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "getopts", "memchr", "pulldown-cmark-escape", @@ -4290,6 +4639,39 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "qrforge" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532d8e082f339eadd546d04e61d809be4b3ca469a75ce86016f89bc5482d5ebb" +dependencies = [ + "image", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quinn" version = "0.11.9" @@ -4431,6 +4813,76 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rcgen" version = "0.14.7" @@ -4451,7 +4903,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -4460,7 +4912,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -4572,6 +5024,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + [[package]] name = "ring" version = "0.17.14" @@ -4691,7 +5149,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -4720,10 +5178,10 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.1", + "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] @@ -4835,24 +5293,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.5.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4861,9 +5306,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" dependencies = [ "core-foundation-sys", "libc", @@ -5170,6 +5615,15 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simple_asn1" version = "0.6.4" @@ -5360,7 +5814,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "bytes", "chrono", @@ -5404,7 +5858,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "chrono", "crc", @@ -5613,9 +6067,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.115" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -5648,7 +6102,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5764,6 +6218,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.4.21", +] + [[package]] name = "time" version = "0.3.47" @@ -5935,9 +6403,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a286e33f82f8a1ee2df63f4fa35c0becf4a85a0cb03091a15fd7bf0b402dc94a" +checksum = "7f32a6f80051a4111560201420c7885d0082ba9efe2ab61875c587bb6b18b9a0" dependencies = [ "async-trait", "axum", @@ -5967,9 +6435,9 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27aac809edf60b741e2d7db6367214d078856b8a5bff0087e94ff330fb97b6fc" +checksum = "ce6d8958ed3be404120ca43ffa0fb1e1fc7be214e96c8d33bd43a131b6eebc9e" dependencies = [ "prettyplease", "proc-macro2", @@ -5979,9 +6447,9 @@ dependencies = [ [[package]] name = "tonic-health" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dbde2c702c4be12b9b2f6f7e6c824a84a7b7be177070cada8ee575a581af359" +checksum = "163e5ad9be2924d9cef75f02fcd44c1803a5af250f4ef7e085992270ac51fb9b" dependencies = [ "prost", "tokio", @@ -5992,9 +6460,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c55a2d6a14174563de34409c9f92ff981d006f56da9c6ecd40d9d4a31500b0" +checksum = "9f86539c0089bfd09b1f8c0ab0239d80392af74c21bc9e0f15e1b4aca4c1647f" dependencies = [ "bytes", "prost", @@ -6003,9 +6471,9 @@ dependencies = [ [[package]] name = "tonic-prost-build" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4556786613791cfef4ed134aa670b61a85cfcacf71543ef33e8d801abae988f" +checksum = "65873ace111e90344b8973e94a1fc817c924473affff24629281f90daed1cd2e" dependencies = [ "prettyplease", "proc-macro2", @@ -6054,7 +6522,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-core", "futures-util", @@ -6216,9 +6684,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -6358,16 +6826,27 @@ checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d" [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.1", "js-sys", "serde_core", "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -6574,7 +7053,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap 2.13.0", "semver", @@ -6722,6 +7201,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "whoami" version = "1.6.1" @@ -7109,7 +7594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.0", "indexmap 2.13.0", "log", "serde", @@ -7201,6 +7686,12 @@ dependencies = [ "time", ] +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yasna" version = "0.5.2" @@ -7365,3 +7856,42 @@ dependencies = [ "log", "simd-adler32", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" +dependencies = [ + "zune-core 0.5.1", +] diff --git a/crates/defguard_mail/Cargo.toml b/crates/defguard_mail/Cargo.toml index 75d5f4a5c5..9ada5ddf2e 100644 --- a/crates/defguard_mail/Cargo.toml +++ b/crates/defguard_mail/Cargo.toml @@ -23,7 +23,9 @@ tokio.workspace = true tracing.workspace = true humantime.workspace = true +image = "0.25" # match with qrforge mrml = "5.1" +qrforge = {version = "0.1", default-features = false, features = ["image"]} [dev-dependencies] claims.workspace = true diff --git a/crates/defguard_mail/src/lib.rs b/crates/defguard_mail/src/lib.rs index 152ad93664..7fe038abdb 100644 --- a/crates/defguard_mail/src/lib.rs +++ b/crates/defguard_mail/src/lib.rs @@ -11,6 +11,7 @@ pub use crate::mail::{Attachment, Mail}; pub mod mail; pub(crate) mod mail_context; +mod qr; pub mod templates; #[cfg(test)] mod tests; diff --git a/crates/defguard_mail/src/mail.rs b/crates/defguard_mail/src/mail.rs index fac3c7a176..de92c002c6 100644 --- a/crates/defguard_mail/src/mail.rs +++ b/crates/defguard_mail/src/mail.rs @@ -8,12 +8,13 @@ use lettre::{ }; use serde::Serialize; use sqlx::PgConnection; -use tera::{Context, Tera}; +use tera::{Context, Tera, Value}; use thiserror::Error; use tracing::{debug, error, info, warn}; use crate::{ mail_context::MailContext, + qr::qr_png, templates::{DEFAULT_LANG, TemplateError}, }; @@ -394,6 +395,11 @@ impl MailMessage { mail.add_png_image("new_account_2", NEW_ACCOUNT_2); mail.add_png_image("google_play", GOOGLE_PLAY); mail.add_png_image("apple", APPLE); + if let Some(Value::String(url)) = context.get("url") { + if let Ok(qr) = qr_png(url.as_bytes()) { + mail.add_png_image("qr", &qr); + } + } } Self::MFACode => { mail.add_png_image("date", DATE_ICON); diff --git a/crates/defguard_mail/src/qr.rs b/crates/defguard_mail/src/qr.rs new file mode 100644 index 0000000000..1957f96b10 --- /dev/null +++ b/crates/defguard_mail/src/qr.rs @@ -0,0 +1,28 @@ +use std::io::Cursor; + +use image::ImageFormat; +use qrforge::{ErrorCorrection, Mode, QRCode, QRError, Version}; + +/// Construct QR with content bytes and return a buffer of PNG image. +pub(crate) fn qr_png(content: &[u8]) -> Result, QRError> { + let qr = QRCode::builder() + .add_segment(Some(Mode::Byte), content) + .error_correction(ErrorCorrection::M) + .version(Version::V(5)) + .build()?; + + let image_buffer = qr + .image_builder() + .set_width(200) + .set_height(200) + .set_border(4) + .build_image()?; + + let mut buffer = Cursor::new(Vec::new()); + + image_buffer + .write_to(&mut buffer, ImageFormat::Png) + .map_err(|_| QRError::new("image write error"))?; + + Ok(buffer.into_inner()) +} diff --git a/crates/defguard_mail/templates/new-account.mjml b/crates/defguard_mail/templates/new-account.mjml index cab0ad0122..17f81e094d 100644 --- a/crates/defguard_mail/templates/new-account.mjml +++ b/crates/defguard_mail/templates/new-account.mjml @@ -69,7 +69,7 @@ {{ scan_qr }} - + {{ mobile_install }} From 9fcb18872a7a9d9a7693a7d08c2c7a140717e4df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 16 Feb 2026 10:42:03 +0100 Subject: [PATCH 5/7] Update crates: pgp, serde_qs --- Cargo.lock | 29 ++++++++++++------- Cargo.toml | 2 +- crates/defguard_core/Cargo.toml | 2 +- .../defguard_core/src/enterprise/license.rs | 26 ++++++++--------- 4 files changed, 33 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2dc4e81954..536531e9fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -558,18 +558,18 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitfields" -version = "0.12.4" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d84268bbf9b487d31fe4b849edbefcd3911422d7a07de855a2da1f70ab3d1c" +checksum = "d866f92dc1574aa8da443eacb06ad8fbe4056dbc1b7c3aae508cbccd46c7e706" dependencies = [ "bitfields-impl", ] [[package]] name = "bitfields-impl" -version = "0.9.4" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c93edde7bb4416c35c85048e34f78999dcb47d199bde3b1d79286156f3e2fb" +checksum = "c09459e6af3016ea58af8332e31d5da117d33a621bad7019355eefccc4a567d4" dependencies = [ "proc-macro2", "quote", @@ -4244,9 +4244,9 @@ dependencies = [ [[package]] name = "pgp" -version = "0.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91d320242d9b686612b15526fe38711afdf856e112eaa4775ce25b0d9b12b11" +checksum = "eaffe1ec22db286599c30ae6be75b37493b558735d86c8e59ec5c38794415fe4" dependencies = [ "aead", "aes", @@ -4263,7 +4263,6 @@ dependencies = [ "camellia", "cast5", "cfb-mode", - "chrono", "cipher", "const-oid", "crc24", @@ -4286,7 +4285,7 @@ dependencies = [ "k256", "log", "md-5", - "nom 7.1.3", + "nom 8.0.0", "num-bigint-dig", "num-traits", "num_enum", @@ -4296,6 +4295,7 @@ dependencies = [ "p521", "rand 0.8.5", "regex", + "replace_with", "ripemd", "rsa", "sha1", @@ -4964,6 +4964,12 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "replace_with" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" + [[package]] name = "reqwest" version = "0.12.28" @@ -5432,13 +5438,14 @@ dependencies = [ [[package]] name = "serde_qs" -version = "0.15.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" +checksum = "ac22439301a0b6f45a037681518e3169e8db1db76080e2e9600a08d1027df037" dependencies = [ + "itoa", "percent-encoding", + "ryu", "serde", - "thiserror 2.0.18", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c816dcc7ce..4da76766c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ openidconnect = { version = "4.0", default-features = false, features = [ ] } parse_link_header = "0.4" paste = "1.0" -pgp = { version = "0.16", default-features = false } +pgp = { version = "0.19", default-features = false } prost = "0.14" pulldown-cmark = "0.13" # match version used by sqlx diff --git a/crates/defguard_core/Cargo.toml b/crates/defguard_core/Cargo.toml index c6d2f82b7d..193eb80d18 100644 --- a/crates/defguard_core/Cargo.toml +++ b/crates/defguard_core/Cargo.toml @@ -94,7 +94,7 @@ reqwest = { version = "0.12", features = [ "rustls-tls", "stream", ], default-features = false } -serde_qs = "0.15" +serde_qs = "1.0" webauthn-authenticator-rs = { version = "0.5", features = ["softpasskey"] } [build-dependencies] diff --git a/crates/defguard_core/src/enterprise/license.rs b/crates/defguard_core/src/enterprise/license.rs index 2f0f8b67e9..dd20f61256 100644 --- a/crates/defguard_core/src/enterprise/license.rs +++ b/crates/defguard_core/src/enterprise/license.rs @@ -11,8 +11,8 @@ use defguard_common::{ }; use humantime::format_duration; use pgp::{ - composed::{Deserializable, SignedPublicKey, StandaloneSignature}, - types::{KeyDetails, PublicKeyTrait}, + composed::{Deserializable, DetachedSignature, SignedPublicKey}, + types::KeyDetails, }; use prost::Message; use sqlx::{PgPool, error::Error as SqlxError}; @@ -269,8 +269,8 @@ impl License { } fn verify_signature(data: &[u8], signature: &[u8]) -> Result<(), LicenseError> { - let sig = StandaloneSignature::from_bytes(signature) - .map_err(|_| LicenseError::InvalidSignature)?; + let sig = + DetachedSignature::from_bytes(signature).map_err(|_| LicenseError::InvalidSignature)?; let (public_key, _headers_public) = SignedPublicKey::from_string(PUBLIC_KEY).expect("Failed to parse the public key"); @@ -279,21 +279,21 @@ impl License { if public_key.public_subkeys.is_empty() { debug!( "Using the public key's primary key {:?} to verify the signature...", - public_key.key_id() + public_key.legacy_key_id() ); sig.verify(&public_key, data) .map_err(|_| LicenseError::SignatureMismatch) } else { - let signing_key = public_key - .public_subkeys - .into_iter() - .find(PublicKeyTrait::is_signing_key) - .ok_or(LicenseError::LicenseServerError( - "Failed to find a signing key in the provided public key".to_string(), - ))?; + let signing_key = + public_key + .public_subkeys + .first() + .ok_or(LicenseError::LicenseServerError( + "Failed to find a signing key in the provided public key".to_string(), + ))?; debug!( "Using the public key's subkey {:?} to verify the signature...", - signing_key.key_id() + signing_key.legacy_key_id() ); sig.verify(&signing_key, data) .map_err(|_| LicenseError::SignatureMismatch) From 2b2df820c839149ceb61a570ecca79439063cd66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 16 Feb 2026 10:51:24 +0100 Subject: [PATCH 6/7] Make clippy happy --- .../src/enrollment_management.rs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/crates/defguard_core/src/enrollment_management.rs b/crates/defguard_core/src/enrollment_management.rs index ef97e666e3..90f96d8d57 100644 --- a/crates/defguard_core/src/enrollment_management.rs +++ b/crates/defguard_core/src/enrollment_management.rs @@ -10,7 +10,7 @@ use crate::db::models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token, TokenError}; /// and optionally sends enrollment email notification to user pub async fn start_user_enrollment( user: &mut User, - mut transaction: &mut PgConnection, + conn: &mut PgConnection, admin: &User, email: Option, token_timeout_seconds: u64, @@ -43,7 +43,7 @@ pub async fn start_user_enrollment( return Err(TokenError::UserDisabled); } - clear_unused_enrollment_tokens(user, &mut *transaction).await?; + clear_unused_enrollment_tokens(user, &mut *conn).await?; debug!("Create a new enrollment token for user {}.", user.username); let enrollment = Token::new( @@ -54,7 +54,7 @@ pub async fn start_user_enrollment( Some(ENROLLMENT_TOKEN_TYPE.to_string()), ); debug!("Saving a new enrollment token..."); - enrollment.save(&mut *transaction).await?; + enrollment.save(&mut *conn).await?; debug!( "Saved a new enrollment token with id {} for user {}.", enrollment.id, user.username @@ -63,7 +63,7 @@ pub async fn start_user_enrollment( // Mark the user with enrollment-pending flag. // https://github.com/DefGuard/client/issues/647 user.enrollment_pending = true; - user.save(&mut *transaction).await?; + user.save(&mut *conn).await?; if send_user_notification { if let Some(email) = email { @@ -71,12 +71,10 @@ pub async fn start_user_enrollment( "Sending an enrollment mail for user {} to {email}.", user.username ); - let base_message_context = enrollment - .get_welcome_message_context(&mut *transaction) - .await?; + let base_message_context = enrollment.get_welcome_message_context(&mut *conn).await?; let result = new_account_mail( &email, - &mut transaction, + conn, base_message_context, enrollment_service_url, &enrollment.id, @@ -109,7 +107,7 @@ pub async fn start_user_enrollment( /// and optionally sends email notification to user pub async fn start_desktop_configuration( user: &User, - mut transaction: &mut PgConnection, + conn: &mut PgConnection, admin: &User, email: Option, token_timeout_seconds: u64, @@ -137,7 +135,7 @@ pub async fn start_desktop_configuration( return Err(TokenError::UserDisabled); } - clear_unused_enrollment_tokens(user, &mut *transaction).await?; + clear_unused_enrollment_tokens(user, &mut *conn).await?; debug!("Cleared unused tokens for {}.", user.username); debug!( @@ -155,7 +153,7 @@ pub async fn start_desktop_configuration( desktop_configuration.device_id = Some(device_id); } debug!("Saving a new desktop configuration token..."); - desktop_configuration.save(&mut *transaction).await?; + desktop_configuration.save(&mut *conn).await?; debug!( "Saved a new desktop activation token with id {} for user {}.", desktop_configuration.id, user.username @@ -168,11 +166,11 @@ pub async fn start_desktop_configuration( user.username ); let base_message_context = desktop_configuration - .get_welcome_message_context(&mut *transaction) + .get_welcome_message_context(&mut *conn) .await?; let result = desktop_start_mail( &email, - &mut transaction, + conn, base_message_context, &enrollment_service_url, &desktop_configuration.id, From 54febc1531411860d5a114bc27050e095442eb16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 16 Feb 2026 11:03:29 +0100 Subject: [PATCH 7/7] Make cargo deny happy --- deny.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deny.toml b/deny.toml index 14216c9e9e..ae7303ded6 100644 --- a/deny.toml +++ b/deny.toml @@ -91,6 +91,7 @@ allow = [ "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "MPL-2.0", + "BSD-2-Clause", "BSD-3-Clause", "Unicode-3.0", "Zlib", @@ -100,6 +101,7 @@ allow = [ "CC0-1.0", "OpenSSL", "CDLA-Permissive-2.0", + "NCSA", ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the