From c8fc01dbf75757061317c9b3f03d9ec6dbded20b Mon Sep 17 00:00:00 2001 From: Emmyt24 Date: Fri, 7 Aug 2026 16:36:28 +0100 Subject: [PATCH] fix: serve email images as real HTTPS URLs instead of inline data: URIs Gmail and most major email clients strip inline data:image/svg+xml (and often data: images generally), so the logo, purpose icons, and social icons all rendered as blank boxes despite looking correct in a browser preview. Switches to real hosted images: the logo on Cloudinary, and the small icon set served from octohq.org/email/ (Octo-frontend's public/email/). Also fixes the welcome email's dashboard link, which pointed at the wrong domain. --- Cargo.lock | 1 - crates/email/Cargo.toml | 1 - crates/email/src/templates.rs | 115 ++++++++++------------------------ 3 files changed, 34 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b088290..c35d45e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1770,7 +1770,6 @@ dependencies = [ name = "octo-email" version = "0.1.0" dependencies = [ - "base64", "chrono", "hex", "rand 0.8.6", diff --git a/crates/email/Cargo.toml b/crates/email/Cargo.toml index 42e1147..20a9e1e 100644 --- a/crates/email/Cargo.toml +++ b/crates/email/Cargo.toml @@ -12,7 +12,6 @@ authors.workspace = true test-fixtures = [] [dependencies] -base64.workspace = true chrono.workspace = true reqwest.workspace = true sha2.workspace = true diff --git a/crates/email/src/templates.rs b/crates/email/src/templates.rs index 1e00e99..11dd603 100644 --- a/crates/email/src/templates.rs +++ b/crates/email/src/templates.rs @@ -1,119 +1,72 @@ //! Email HTML templates. Keep in sync with Octo-frontend's `src/emails/` — that's the //! hand-maintained source; this is the copy actually sent. - -use base64::Engine; +//! +//! Images are real HTTPS URLs, not inline data: URIs — Gmail and most clients strip inline +//! data:image/svg+xml (and often data: images generally), so anything shown here has to be +//! fetchable. The logo lives on Cloudinary; the small icon set is served from octohq.org/email/ +//! (Octo-frontend's public/email/). const BURGUNDY: &str = "#7b1733"; const BURGUNDY_BRIGHT: &str = "#b81f4d"; const INK: &str = "#0a0506"; -/// Octo's octopus mark, resolved to static colors (no CSS vars — email clients won't resolve them). -fn logo_svg() -> String { - format!( - "\ -\ -\ -\ -\ -\ -\ -\ -\ -" - ) +const LOGO_URL: &str = + "https://res.cloudinary.com/h9jpvcxe/image/upload/v1786115234/octopus_burgundy_black_bg_wrx0sf.png"; +const ICON_BASE: &str = "https://octohq.org/email"; + +/// A purpose icon shown above the body copy: a colored circle badge with a glyph. +enum Icon { + Key, + Wave, + Check, + Warn, } -fn svg_data_uri(svg: &str) -> String { - format!( - "data:image/svg+xml;base64,{}", - base64::engine::general_purpose::STANDARD.encode(svg) - ) +fn icon_url(icon: Icon) -> &'static str { + match icon { + Icon::Key => "icon-key.png", + Icon::Wave => "icon-wave.png", + Icon::Check => "icon-check.png", + Icon::Warn => "icon-warn.png", + } } -/// A social icon: white glyph on a filled burgundy circle, matching the button's brand color. -fn social_icon(path: &str, href: &str, label: &str) -> String { - let svg = format!( - "\ -\ -{path}\ -" - ); - let uri = svg_data_uri(&svg); +/// A social icon link: white glyph on a filled burgundy circle, matching the button's brand color. +fn social_icon(icon: &str, href: &str, label: &str) -> String { format!( - "\"{label}\"" + "\"{label}\"" ) } fn socials() -> String { [ + social_icon("x", "https://x.com/Octo_Hq", "X (Twitter)"), + social_icon("instagram", "https://instagram.com/OctoHQ", "Instagram"), social_icon( - "", - "https://x.com/Octo_Hq", - "X (Twitter)", - ), - social_icon( - "", - "https://instagram.com/OctoHQ", - "Instagram", - ), - social_icon( - "", + "linkedin", "https://linkedin.com/company/OctoHQ", "LinkedIn", ), - social_icon( - "", - "https://github.com/Octo-Protocol-org", - "GitHub", - ), - social_icon( - "", - "https://t.me/OctoHQ", - "Telegram", - ), + social_icon("github", "https://github.com/Octo-Protocol-org", "GitHub"), + social_icon("telegram", "https://t.me/OctoHQ", "Telegram"), ] .join("") } -/// A purpose icon shown above the body copy: a colored circle badge with a glyph. -enum Icon { - Key, - Wave, - Check, - Warn, -} - -fn icon_svg(icon: Icon) -> String { - let inner = match icon { - Icon::Key => format!( - "" - ), - Icon::Wave => format!( - "" - ), - Icon::Check => "".to_string(), - Icon::Warn => "".to_string(), - }; - svg_data_uri(&format!( - "{inner}" - )) -} - fn shell(body: &str, icon: Icon) -> String { - let logo_uri = svg_data_uri(&logo_svg()); - let icon_uri = icon_svg(icon); + let icon_url = format!("{ICON_BASE}/{}", icon_url(icon)); let socials = socials(); let year = chrono::Utc::now().format("%Y"); format!( "
\
\
\ -\"Octo\"\ +\"Octo\"\
Octo
\
\
\
\ -
\"\"
\ +
\"\"
\ {body}\
\
\ @@ -150,7 +103,7 @@ pub fn welcome_email(email: &str) -> String { &format!( "

Welcome to Octo 🎉

\

{email} is verified and ready to go.

\ -Go to dashboard" +Go to dashboard" ), Icon::Wave, )