diff --git a/.build_number b/.build_number index e41d14f..49673e2 100644 --- a/.build_number +++ b/.build_number @@ -1 +1 @@ -0303261719 +0303261841 diff --git a/backend/app/build_info.py b/backend/app/build_info.py index 3f5a023..1bb755f 100644 --- a/backend/app/build_info.py +++ b/backend/app/build_info.py @@ -1,2 +1,2 @@ -BUILD_NUMBER = "0303261719" -CHANGELOG = '2026-03-03|Update all email templates with uniform branded graphics\n2026-03-03|Add branded HTML email templates\n2026-03-03|Add SMTP receipt logging for Exchange relay tracing\n2026-03-03|Fix shared request access and Jellyfin-ready pipeline status\n2026-03-03|Process 1 build 0303261507\n2026-03-03|Improve SQLite batching and diagnostics visibility\n2026-03-03|Add login page visibility controls\n2026-03-03|Hotfix: expand landing-page search to all requests\n2026-03-02|Hotfix: add logged-out password reset flow\n2026-03-02|Process 1 build 0203261953\n2026-03-02|Process 1 build 0203261610\n2026-03-02|Process 1 build 0203261608\n2026-03-02|Add dedicated profile invites page and fix mobile admin layout\n2026-03-01|Persist Seerr media failure suppression and reduce sync error noise\n2026-03-01|Add repository line ending policy\n2026-03-01|Finalize diagnostics, logging controls, and email test support\n2026-03-01|Add invite email templates and delivery workflow\n2026-02-28|Finalize dev-1.3 upgrades and Seerr updates\n2026-02-27|admin docs and layout refresh, build 2702261314\n2026-02-27|Build 2702261153: fix jellyfin sync user visibility\n2026-02-26|Build 2602262241: live request page updates\n2026-02-26|Build 2602262204\n2026-02-26|Build 2602262159: restore jellyfin-first user source\n2026-02-26|Build 2602262049: split magent settings and harden local login\n2026-02-26|Build 2602262030: add magent settings and hardening\n2026-02-26|Build 2602261731: fix user resync after nuclear wipe\n2026-02-26|Build 2602261717: master invite policy and self-service invite controls\n2026-02-26|Build 2602261636: self-service invites and count fixes\n2026-02-26|Build 2602261605: invite trace and cross-system user lifecycle\n2026-02-26|Build 2602261536: refine invite layouts and tighten UI\n2026-02-26|Build 2602261523: live updates, invite cleanup and nuclear resync\n2026-02-26|Build 2602261442: tidy users and invite layouts\n2026-02-26|Build 2602261409: unify invite management controls\n2026-02-26|Build 2602260214: invites profiles and expiry admin controls\n2026-02-26|Build 2602260022: enterprise UI refresh and users bulk auto-search\n2026-02-25|Build 2502262321: fix auto-search quality and per-user toggle\n2026-02-02|Build 0202261541: allow FQDN service URLs\n2026-01-30|Build 3001262148: single container\n2026-01-29|Build 2901262244: format changelog\n2026-01-29|Build 2901262240: cache users\n2026-01-29|Tidy full changelog\n2026-01-29|Update full changelog\n2026-01-29|Bake build number and changelog\n2026-01-29|Hardcode build number in backend\n2026-01-29|release: 2901262102\n2026-01-29|release: 2901262044\n2026-01-29|release: 2901262036\n2026-01-27|Hydrate missing artwork from Jellyseerr (build 271261539)\n2026-01-27|Fallback to TMDB when artwork cache fails (build 271261524)\n2026-01-27|Add service test buttons (build 271261335)\n2026-01-27|Bump build number (process 2) 271261322\n2026-01-27|Add cache load spinner (build 271261238)\n2026-01-27|Fix snapshot title fallback (build 271261228)\n2026-01-27|Fix request titles in snapshots (build 271261219)\n2026-01-27|Bump build number to 271261202\n2026-01-27|Clarify request sync settings (build 271261159)\n2026-01-27|Fix backend cache stats import (build 271261149)\n2026-01-27|Improve cache stats performance (build 271261145)\n2026-01-27|Add cache control artwork stats\n2026-01-26|Fix sync progress bar animation\n2026-01-26|Fix cache title hydration\n2026-01-25|Build 2501262041\n2026-01-25|Harden request cache titles and cache-only reads\n2026-01-25|Serve bundled branding assets by default\n2026-01-25|Seed branding logo from bundled assets\n2026-01-25|Tidy request sync controls\n2026-01-25|Add Jellyfin login cache and admin-only stats\n2026-01-25|Add user stats and activity tracking\n2026-01-25|Move account actions into avatar menu\n2026-01-25|Improve mobile header layout\n2026-01-25|Automate build number tagging and sync\n2026-01-25|Add site banner, build number, and changelog\n2026-01-24|Improve request handling and qBittorrent categories\n2026-01-24|Map Prowlarr releases to Arr indexers for manual grab\n2026-01-24|Clarify how-it-works steps and fixes\n2026-01-24|Document fix buttons in how-it-works\n2026-01-24|Route grabs through Sonarr/Radarr only\n2026-01-23|Use backend branding assets for logo and favicon\n2026-01-23|Copy public assets into frontend image\n2026-01-23|Fix backend Dockerfile paths for root context\n2026-01-23|Add Docker Hub compose override\n2026-01-23|Remove password fields from users page\n2026-01-23|Use bundled branding assets\n2026-01-23|Add default branding assets when missing\n2026-01-23|Show available status on landing when in Jellyfin\n2026-01-23|Fix cache titles and move feedback link\n2026-01-23|Add feedback form and webhook\n2026-01-23|Hide header actions when signed out\n2026-01-23|Fallback manual grab to qBittorrent\n2026-01-23|Split search actions and improve download options\n2026-01-23|Fix cache titles via Jellyseerr media lookup\n2026-01-22|Update README with Docker-first guide\n2026-01-22|Update README\n2026-01-22|Ignore build artifacts\n2026-01-22|Initial commit' +BUILD_NUMBER = "0303261841" +CHANGELOG = '2026-03-03|Fix email template rendering for Outlook-safe branded content\n2026-03-03|Update all email templates with uniform branded graphics\n2026-03-03|Add branded HTML email templates\n2026-03-03|Add SMTP receipt logging for Exchange relay tracing\n2026-03-03|Fix shared request access and Jellyfin-ready pipeline status\n2026-03-03|Process 1 build 0303261507\n2026-03-03|Improve SQLite batching and diagnostics visibility\n2026-03-03|Add login page visibility controls\n2026-03-03|Hotfix: expand landing-page search to all requests\n2026-03-02|Hotfix: add logged-out password reset flow\n2026-03-02|Process 1 build 0203261953\n2026-03-02|Process 1 build 0203261610\n2026-03-02|Process 1 build 0203261608\n2026-03-02|Add dedicated profile invites page and fix mobile admin layout\n2026-03-01|Persist Seerr media failure suppression and reduce sync error noise\n2026-03-01|Add repository line ending policy\n2026-03-01|Finalize diagnostics, logging controls, and email test support\n2026-03-01|Add invite email templates and delivery workflow\n2026-02-28|Finalize dev-1.3 upgrades and Seerr updates\n2026-02-27|admin docs and layout refresh, build 2702261314\n2026-02-27|Build 2702261153: fix jellyfin sync user visibility\n2026-02-26|Build 2602262241: live request page updates\n2026-02-26|Build 2602262204\n2026-02-26|Build 2602262159: restore jellyfin-first user source\n2026-02-26|Build 2602262049: split magent settings and harden local login\n2026-02-26|Build 2602262030: add magent settings and hardening\n2026-02-26|Build 2602261731: fix user resync after nuclear wipe\n2026-02-26|Build 2602261717: master invite policy and self-service invite controls\n2026-02-26|Build 2602261636: self-service invites and count fixes\n2026-02-26|Build 2602261605: invite trace and cross-system user lifecycle\n2026-02-26|Build 2602261536: refine invite layouts and tighten UI\n2026-02-26|Build 2602261523: live updates, invite cleanup and nuclear resync\n2026-02-26|Build 2602261442: tidy users and invite layouts\n2026-02-26|Build 2602261409: unify invite management controls\n2026-02-26|Build 2602260214: invites profiles and expiry admin controls\n2026-02-26|Build 2602260022: enterprise UI refresh and users bulk auto-search\n2026-02-25|Build 2502262321: fix auto-search quality and per-user toggle\n2026-02-02|Build 0202261541: allow FQDN service URLs\n2026-01-30|Build 3001262148: single container\n2026-01-29|Build 2901262244: format changelog\n2026-01-29|Build 2901262240: cache users\n2026-01-29|Tidy full changelog\n2026-01-29|Update full changelog\n2026-01-29|Bake build number and changelog\n2026-01-29|Hardcode build number in backend\n2026-01-29|release: 2901262102\n2026-01-29|release: 2901262044\n2026-01-29|release: 2901262036\n2026-01-27|Hydrate missing artwork from Jellyseerr (build 271261539)\n2026-01-27|Fallback to TMDB when artwork cache fails (build 271261524)\n2026-01-27|Add service test buttons (build 271261335)\n2026-01-27|Bump build number (process 2) 271261322\n2026-01-27|Add cache load spinner (build 271261238)\n2026-01-27|Fix snapshot title fallback (build 271261228)\n2026-01-27|Fix request titles in snapshots (build 271261219)\n2026-01-27|Bump build number to 271261202\n2026-01-27|Clarify request sync settings (build 271261159)\n2026-01-27|Fix backend cache stats import (build 271261149)\n2026-01-27|Improve cache stats performance (build 271261145)\n2026-01-27|Add cache control artwork stats\n2026-01-26|Fix sync progress bar animation\n2026-01-26|Fix cache title hydration\n2026-01-25|Build 2501262041\n2026-01-25|Harden request cache titles and cache-only reads\n2026-01-25|Serve bundled branding assets by default\n2026-01-25|Seed branding logo from bundled assets\n2026-01-25|Tidy request sync controls\n2026-01-25|Add Jellyfin login cache and admin-only stats\n2026-01-25|Add user stats and activity tracking\n2026-01-25|Move account actions into avatar menu\n2026-01-25|Improve mobile header layout\n2026-01-25|Automate build number tagging and sync\n2026-01-25|Add site banner, build number, and changelog\n2026-01-24|Improve request handling and qBittorrent categories\n2026-01-24|Map Prowlarr releases to Arr indexers for manual grab\n2026-01-24|Clarify how-it-works steps and fixes\n2026-01-24|Document fix buttons in how-it-works\n2026-01-24|Route grabs through Sonarr/Radarr only\n2026-01-23|Use backend branding assets for logo and favicon\n2026-01-23|Copy public assets into frontend image\n2026-01-23|Fix backend Dockerfile paths for root context\n2026-01-23|Add Docker Hub compose override\n2026-01-23|Remove password fields from users page\n2026-01-23|Use bundled branding assets\n2026-01-23|Add default branding assets when missing\n2026-01-23|Show available status on landing when in Jellyfin\n2026-01-23|Fix cache titles and move feedback link\n2026-01-23|Add feedback form and webhook\n2026-01-23|Hide header actions when signed out\n2026-01-23|Fallback manual grab to qBittorrent\n2026-01-23|Split search actions and improve download options\n2026-01-23|Fix cache titles via Jellyseerr media lookup\n2026-01-22|Update README with Docker-first guide\n2026-01-22|Update README\n2026-01-22|Ignore build artifacts\n2026-01-22|Initial commit' diff --git a/backend/app/services/invite_email.py b/backend/app/services/invite_email.py index 1cfb4d6..788f6ee 100644 --- a/backend/app/services/invite_email.py +++ b/backend/app/services/invite_email.py @@ -6,9 +6,12 @@ import json import logging import re import smtplib +from functools import lru_cache +from pathlib import Path from email.generator import BytesGenerator from email.message import EmailMessage -from email.utils import formataddr +from email.policy import SMTP as SMTP_POLICY +from email.utils import formataddr, formatdate, make_msgid from io import BytesIO from typing import Any, Dict, Optional @@ -25,6 +28,7 @@ EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") PLACEHOLDER_PATTERN = re.compile(r"{{\s*([a-zA-Z0-9_]+)\s*}}") EXCHANGE_MESSAGE_ID_PATTERN = re.compile(r"<([^>]+)>") EXCHANGE_INTERNAL_ID_PATTERN = re.compile(r"\[InternalId=([^\],]+)") +EMAIL_LOGO_CID = "magent-logo" TEMPLATE_METADATA: Dict[str, Dict[str, Any]] = { "invited": { @@ -145,14 +149,15 @@ def _build_email_stat_card(label: str, value: str, detail: str = "") -> str: else "" ) return ( - "
" - f"
{html.escape(label)}
" + "" + "
" + f"
" + f"{html.escape(label)}
" f"
" f"{html.escape(value)}
" f"{detail_html}" - "" + "
" ) @@ -229,12 +234,14 @@ def _build_email_panel(title: str, body_html: str, *, variant: str = "neutral") "text": "#132033", }) return ( - f"
" - f"
{html.escape(title)}
" + "" + f"
" + f"
" + f"{html.escape(title)}
" f"
{body_html}
" - "" + "
" ) @@ -519,6 +526,129 @@ def _build_email_action_button(label: str, url: str, *, primary: bool) -> str: ) +@lru_cache(maxsize=1) +def _get_email_logo_bytes() -> bytes: + logo_path = Path(__file__).resolve().parents[1] / "assets" / "branding" / "logo.png" + try: + return logo_path.read_bytes() + except OSError: + return b"" + + +def _build_email_logo_block(app_name: str) -> str: + if _get_email_logo_bytes(): + return ( + f"\"{html.escape(app_name)}\"" + ) + return ( + "
M
" + ) + + +def _build_outlook_safe_test_email_html( + *, + app_name: str, + application_url: str, + build_number: str, + smtp_target: str, + security_mode: str, + auth_mode: str, + warning: str, + primary_url: str = "", +) -> str: + action_html = ( + _build_email_action_button("Open Magent", primary_url, primary=True) if primary_url else "" + ) + logo_block = _build_email_logo_block(app_name) + warning_block = ( + "" + "" + "" + "
" + "
" + "Delivery notes
" + f"{html.escape(warning)}" + "
" + "" + "" + ) if warning else "" + return ( + "" + "" + "" + "" + "
" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + f"{warning_block}" + "" + "" + "" + "
" + "" + "" + f"" + "" + "" + "
{logo_block}" + f"
{html.escape(app_name)} email test
" + "
This confirms Magent can generate and hand off branded mail.
" + "
" + "
 
" + "
This is a test email from Magent.
" + "
" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "
" + f"{_build_email_stat_card('Build', build_number)}" + "" + f"{_build_email_stat_card('Application URL', application_url)}" + "
" + f"{_build_email_stat_card('SMTP target', smtp_target)}" + "" + f"{_build_email_stat_card('Security', security_mode, auth_mode)}" + "
" + "
" + "" + "
" + "
" + "What this verifies
" + "
Magent can build the HTML template shell correctly.
" + "
The configured SMTP route accepts and relays the message.
" + "
Branding, links, and build metadata are rendering consistently.
" + "
" + "
" + f"{action_html}" + "
" + "
" + "" + "" + ) + + def _wrap_email_html( *, app_name: str, @@ -535,10 +665,6 @@ def _wrap_email_html( footer_note: str = "", ) -> str: styles = EMAIL_TONE_STYLES.get(tone, EMAIL_TONE_STYLES["brand"]) - logo_url = "" - if app_url.lower().startswith("http://") or app_url.lower().startswith("https://"): - logo_url = f"{app_url.rstrip('/')}/branding/logo.png" - actions = [] if primary_label and primary_url: actions.append(_build_email_action_button(primary_label, primary_url, primary=True)) @@ -547,17 +673,7 @@ def _wrap_email_html( actions_html = "".join(actions) footer = footer_note or "This email was generated automatically by Magent." - logo_block = ( - f"\"{html.escape(app_name)}\"" - if logo_url - else ( - "
M
" - ) - ) + logo_block = _build_email_logo_block(app_name) return ( "" @@ -803,7 +919,7 @@ def smtp_email_delivery_warning() -> Optional[str]: def _flatten_message(message: EmailMessage) -> bytes: buffer = BytesIO() - BytesGenerator(buffer).flatten(message) + BytesGenerator(buffer, policy=SMTP_POLICY).flatten(message) return buffer.getvalue() @@ -881,9 +997,27 @@ def _send_email_sync(*, recipient_email: str, subject: str, body_text: str, body message["Subject"] = subject message["From"] = formataddr((from_name, from_address)) message["To"] = recipient_email + message["Date"] = formatdate(localtime=True) + if "@" in from_address: + message["Message-ID"] = make_msgid(domain=from_address.split("@", 1)[1]) + else: + message["Message-ID"] = make_msgid() message.set_content(body_text or _strip_html_for_text(body_html)) if body_html.strip(): message.add_alternative(body_html, subtype="html") + if f"cid:{EMAIL_LOGO_CID}" in body_html: + logo_bytes = _get_email_logo_bytes() + if logo_bytes: + html_part = message.get_body(preferencelist=("html",)) + if html_part is not None: + html_part.add_related( + logo_bytes, + maintype="image", + subtype="png", + cid=f"<{EMAIL_LOGO_CID}>", + filename="logo.png", + disposition="inline", + ) if use_ssl: with smtplib.SMTP_SSL(host, port, timeout=20) as smtp: @@ -1006,6 +1140,12 @@ async def send_test_email(recipient_email: Optional[str] = None) -> Dict[str, st f"This is a test email from {env_settings.app_name}.\n\n" f"Build: {BUILD_NUMBER}\n" f"Application URL: {application_url}\n" + f"SMTP target: {smtp_target}\n" + f"Security: {security_mode} ({auth_mode})\n\n" + "What this verifies:\n" + "- Magent can build the HTML template shell correctly.\n" + "- The configured SMTP route accepts and relays the message.\n" + "- Branding, links, and build metadata are rendering consistently.\n" ) body_html = _wrap_email_html( app_name=env_settings.app_name, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4ddbf15..50bc6c1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "magent-frontend", - "version": "0303261719", + "version": "0303261841", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "magent-frontend", - "version": "0303261719", + "version": "0303261841", "dependencies": { "next": "16.1.6", "react": "19.2.4", diff --git a/frontend/package.json b/frontend/package.json index 3271914..d15ae84 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "magent-frontend", "private": true, - "version": "0303261719", + "version": "0303261841", "scripts": { "dev": "next dev", "build": "next build",