Fix email branding with inline logo and reliable MIME transport

This commit is contained in:
2026-03-03 18:42:08 +13:00
parent caa6aa76d6
commit 1ad4823830
5 changed files with 173 additions and 33 deletions

View File

@@ -1 +1 @@
0303261719 0303261841

File diff suppressed because one or more lines are too long

View File

@@ -6,9 +6,12 @@ import json
import logging import logging
import re import re
import smtplib import smtplib
from functools import lru_cache
from pathlib import Path
from email.generator import BytesGenerator from email.generator import BytesGenerator
from email.message import EmailMessage 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 io import BytesIO
from typing import Any, Dict, Optional 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*}}") PLACEHOLDER_PATTERN = re.compile(r"{{\s*([a-zA-Z0-9_]+)\s*}}")
EXCHANGE_MESSAGE_ID_PATTERN = re.compile(r"<([^>]+)>") EXCHANGE_MESSAGE_ID_PATTERN = re.compile(r"<([^>]+)>")
EXCHANGE_INTERNAL_ID_PATTERN = re.compile(r"\[InternalId=([^\],]+)") EXCHANGE_INTERNAL_ID_PATTERN = re.compile(r"\[InternalId=([^\],]+)")
EMAIL_LOGO_CID = "magent-logo"
TEMPLATE_METADATA: Dict[str, Dict[str, Any]] = { TEMPLATE_METADATA: Dict[str, Dict[str, Any]] = {
"invited": { "invited": {
@@ -145,14 +149,15 @@ def _build_email_stat_card(label: str, value: str, detail: str = "") -> str:
else "" else ""
) )
return ( return (
"<div style=\"padding:16px; background:#f8fafc; border:1px solid #d9e2ef; " "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
"border-radius:16px; color:#132033;\">" "style=\"border-collapse:separate; background:#f8fafc; border:1px solid #d9e2ef; border-radius:16px;\">"
f"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#6b778c; " "<tr><td style=\"padding:16px; font-family:Segoe UI, Arial, sans-serif; color:#132033;\">"
f"margin-bottom:8px;\">{html.escape(label)}</div>" f"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#6b778c; margin-bottom:8px;\">"
f"{html.escape(label)}</div>"
f"<div style=\"font-size:20px; font-weight:800; line-height:1.45; word-break:break-word; color:#132033;\">" f"<div style=\"font-size:20px; font-weight:800; line-height:1.45; word-break:break-word; color:#132033;\">"
f"{html.escape(value)}</div>" f"{html.escape(value)}</div>"
f"{detail_html}" f"{detail_html}"
"</div>" "</td></tr></table>"
) )
@@ -229,12 +234,14 @@ def _build_email_panel(title: str, body_html: str, *, variant: str = "neutral")
"text": "#132033", "text": "#132033",
}) })
return ( return (
f"<div style=\"margin:0 0 18px; padding:18px; background:{styles['background']}; " "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
f"border:1px solid {styles['border']}; border-radius:18px; color:{styles['text']};\">" f"style=\"border-collapse:separate; margin:0 0 18px; background:{styles['background']}; "
f"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:{styles['eyebrow']}; " f"border:1px solid {styles['border']}; border-radius:18px;\">"
f"margin-bottom:10px;\">{html.escape(title)}</div>" f"<tr><td style=\"padding:18px; font-family:Segoe UI, Arial, sans-serif; color:{styles['text']};\">"
f"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:{styles['eyebrow']}; margin-bottom:10px;\">"
f"{html.escape(title)}</div>"
f"<div style=\"font-size:14px; line-height:1.8; color:{styles['text']};\">{body_html}</div>" f"<div style=\"font-size:14px; line-height:1.8; color:{styles['text']};\">{body_html}</div>"
"</div>" "</td></tr></table>"
) )
@@ -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"<img src=\"cid:{EMAIL_LOGO_CID}\" alt=\"{html.escape(app_name)}\" width=\"52\" height=\"52\" "
"style=\"display:block; width:52px; height:52px; border-radius:14px; border:1px solid rgba(255,255,255,0.08); "
"background:#0f1522; padding:6px;\" />"
)
return (
"<div style=\"width:52px; height:52px; border-radius:14px; border:1px solid rgba(255,255,255,0.08); "
"background:linear-gradient(135deg, #ff6b2b 0%, #1c6bff 100%); color:#ffffff; font-size:24px; "
"font-weight:900; text-align:center; line-height:52px;\">M</div>"
)
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 = (
"<tr>"
"<td style=\"padding:0 32px 24px 32px; font-family:Segoe UI, Arial, sans-serif;\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
"style=\"border-collapse:separate; background:#fff5ea; border:1px solid #ffd5a8; border-radius:14px;\">"
"<tr><td style=\"padding:16px; color:#132033; font-size:14px; line-height:1.7;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#c46a10; margin-bottom:8px;\">"
"Delivery notes</div>"
f"{html.escape(warning)}"
"</td></tr></table>"
"</td>"
"</tr>"
) if warning else ""
return (
"<!doctype html>"
"<html>"
"<body style=\"margin:0; padding:0; background:#eef2f7;\" bgcolor=\"#eef2f7\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
"style=\"border-collapse:collapse; background:#eef2f7;\" bgcolor=\"#eef2f7\">"
"<tr><td align=\"center\" style=\"padding:32px 16px;\">"
"<table role=\"presentation\" width=\"680\" cellspacing=\"0\" cellpadding=\"0\" "
"style=\"width:680px; max-width:680px; border-collapse:collapse; background:#ffffff; border:1px solid #d5deed;\">"
"<tr><td style=\"padding:24px 32px; background:#0f172a;\" bgcolor=\"#0f172a\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;\">"
"<tr>"
f"<td width=\"56\" valign=\"middle\">{logo_block}</td>"
"<td valign=\"middle\" style=\"padding-left:16px; font-family:Segoe UI, Arial, sans-serif; color:#ffffff;\">"
f"<div style=\"font-size:28px; line-height:1.1; font-weight:800; color:#ffffff;\">{html.escape(app_name)} email test</div>"
"<div style=\"margin-top:6px; font-size:15px; line-height:1.5; color:#d5deed;\">This confirms Magent can generate and hand off branded mail.</div>"
"</td>"
"</tr>"
"</table>"
"</td></tr>"
"<tr><td height=\"6\" style=\"background:#ff6b2b; font-size:0; line-height:0;\">&nbsp;</td></tr>"
"<tr><td style=\"padding:28px 32px 18px 32px; font-family:Segoe UI, Arial, sans-serif; color:#132033;\">"
"<div style=\"font-size:18px; line-height:1.6; color:#132033;\">This is a test email from <strong>Magent</strong>.</div>"
"</td></tr>"
"<tr>"
"<td style=\"padding:0 32px 18px 32px; font-family:Segoe UI, Arial, sans-serif;\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;\">"
"<tr>"
"<td width=\"50%\" valign=\"top\" style=\"padding:0 8px 12px 0;\">"
f"{_build_email_stat_card('Build', build_number)}"
"</td>"
"<td width=\"50%\" valign=\"top\" style=\"padding:0 0 12px 8px;\">"
f"{_build_email_stat_card('Application URL', application_url)}"
"</td>"
"</tr>"
"<tr>"
"<td width=\"50%\" valign=\"top\" style=\"padding:0 8px 12px 0;\">"
f"{_build_email_stat_card('SMTP target', smtp_target)}"
"</td>"
"<td width=\"50%\" valign=\"top\" style=\"padding:0 0 12px 8px;\">"
f"{_build_email_stat_card('Security', security_mode, auth_mode)}"
"</td>"
"</tr>"
"</table>"
"</td>"
"</tr>"
"<tr>"
"<td style=\"padding:0 32px 24px 32px; font-family:Segoe UI, Arial, sans-serif;\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
"style=\"border-collapse:separate; background:#eef4ff; border:1px solid #bfd2ff; border-radius:14px;\">"
"<tr><td style=\"padding:16px; color:#132033; font-size:14px; line-height:1.8;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#2754b6; margin-bottom:8px;\">"
"What this verifies</div>"
"<div>Magent can build the HTML template shell correctly.</div>"
"<div>The configured SMTP route accepts and relays the message.</div>"
"<div>Branding, links, and build metadata are rendering consistently.</div>"
"</td></tr></table>"
"</td>"
"</tr>"
f"{warning_block}"
"<tr>"
"<td style=\"padding:0 32px 32px 32px; font-family:Segoe UI, Arial, sans-serif;\">"
f"{action_html}"
"</td>"
"</tr>"
"</table>"
"</td></tr></table>"
"</body>"
"</html>"
)
def _wrap_email_html( def _wrap_email_html(
*, *,
app_name: str, app_name: str,
@@ -535,10 +665,6 @@ def _wrap_email_html(
footer_note: str = "", footer_note: str = "",
) -> str: ) -> str:
styles = EMAIL_TONE_STYLES.get(tone, EMAIL_TONE_STYLES["brand"]) 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 = [] actions = []
if primary_label and primary_url: if primary_label and primary_url:
actions.append(_build_email_action_button(primary_label, primary_url, primary=True)) actions.append(_build_email_action_button(primary_label, primary_url, primary=True))
@@ -547,17 +673,7 @@ def _wrap_email_html(
actions_html = "".join(actions) actions_html = "".join(actions)
footer = footer_note or "This email was generated automatically by Magent." footer = footer_note or "This email was generated automatically by Magent."
logo_block = ( logo_block = _build_email_logo_block(app_name)
f"<img src=\"{html.escape(logo_url)}\" alt=\"{html.escape(app_name)}\" width=\"52\" height=\"52\" "
"style=\"display:block; width:52px; height:52px; border-radius:14px; border:1px solid rgba(255,255,255,0.08); "
"background:#0f1522; padding:6px;\" />"
if logo_url
else (
"<div style=\"width:52px; height:52px; border-radius:14px; border:1px solid rgba(255,255,255,0.08); "
"background:linear-gradient(135deg, #ff6b2b 0%, #1c6bff 100%); color:#ffffff; font-size:24px; "
"font-weight:900; text-align:center; line-height:52px;\">M</div>"
)
)
return ( return (
"<!doctype html>" "<!doctype html>"
@@ -803,7 +919,7 @@ def smtp_email_delivery_warning() -> Optional[str]:
def _flatten_message(message: EmailMessage) -> bytes: def _flatten_message(message: EmailMessage) -> bytes:
buffer = BytesIO() buffer = BytesIO()
BytesGenerator(buffer).flatten(message) BytesGenerator(buffer, policy=SMTP_POLICY).flatten(message)
return buffer.getvalue() return buffer.getvalue()
@@ -881,9 +997,27 @@ def _send_email_sync(*, recipient_email: str, subject: str, body_text: str, body
message["Subject"] = subject message["Subject"] = subject
message["From"] = formataddr((from_name, from_address)) message["From"] = formataddr((from_name, from_address))
message["To"] = recipient_email 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)) message.set_content(body_text or _strip_html_for_text(body_html))
if body_html.strip(): if body_html.strip():
message.add_alternative(body_html, subtype="html") 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: if use_ssl:
with smtplib.SMTP_SSL(host, port, timeout=20) as smtp: 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"This is a test email from {env_settings.app_name}.\n\n"
f"Build: {BUILD_NUMBER}\n" f"Build: {BUILD_NUMBER}\n"
f"Application URL: {application_url}\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( body_html = _wrap_email_html(
app_name=env_settings.app_name, app_name=env_settings.app_name,

View File

@@ -1,12 +1,12 @@
{ {
"name": "magent-frontend", "name": "magent-frontend",
"version": "0303261719", "version": "0303261841",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "magent-frontend", "name": "magent-frontend",
"version": "0303261719", "version": "0303261841",
"dependencies": { "dependencies": {
"next": "16.1.6", "next": "16.1.6",
"react": "19.2.4", "react": "19.2.4",

View File

@@ -1,7 +1,7 @@
{ {
"name": "magent-frontend", "name": "magent-frontend",
"private": true, "private": true,
"version": "0303261719", "version": "0303261841",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",