Add branded HTML email templates

This commit is contained in:
2026-03-03 16:30:02 +13:00
parent 4f2b5e0922
commit 1ff54690fc
5 changed files with 346 additions and 50 deletions

View File

@@ -1 +1 @@
0303261611
0303261629

File diff suppressed because one or more lines are too long

View File

@@ -64,6 +64,78 @@ TEMPLATE_PLACEHOLDERS = [
"username",
]
EMAIL_TAGLINE = "Find and fix media requests fast."
EMAIL_TONE_STYLES: Dict[str, Dict[str, str]] = {
"brand": {
"chip_bg": "rgba(255, 107, 43, 0.16)",
"chip_border": "rgba(255, 107, 43, 0.38)",
"chip_text": "#ffd2bf",
"accent_a": "#ff6b2b",
"accent_b": "#1c6bff",
},
"success": {
"chip_bg": "rgba(34, 197, 94, 0.16)",
"chip_border": "rgba(34, 197, 94, 0.38)",
"chip_text": "#c7f9d7",
"accent_a": "#22c55e",
"accent_b": "#1c6bff",
},
"warning": {
"chip_bg": "rgba(251, 146, 60, 0.16)",
"chip_border": "rgba(251, 146, 60, 0.38)",
"chip_text": "#ffe0ba",
"accent_a": "#fb923c",
"accent_b": "#ff6b2b",
},
"danger": {
"chip_bg": "rgba(248, 113, 113, 0.16)",
"chip_border": "rgba(248, 113, 113, 0.38)",
"chip_text": "#ffd0d0",
"accent_a": "#ef4444",
"accent_b": "#ff6b2b",
},
}
TEMPLATE_PRESENTATION: Dict[str, Dict[str, str]] = {
"invited": {
"tone": "brand",
"title": "You have been invited",
"subtitle": "A new account invitation is ready for you.",
"primary_label": "Accept invite",
"primary_url_key": "invite_link",
"secondary_label": "How it works",
"secondary_url_key": "how_it_works_url",
},
"welcome": {
"tone": "success",
"title": "Welcome to Magent",
"subtitle": "Your account is ready and synced.",
"primary_label": "Open Magent",
"primary_url_key": "app_url",
"secondary_label": "How it works",
"secondary_url_key": "how_it_works_url",
},
"warning": {
"tone": "warning",
"title": "Account warning",
"subtitle": "Please review the note below.",
"primary_label": "Open Magent",
"primary_url_key": "app_url",
"secondary_label": "How it works",
"secondary_url_key": "how_it_works_url",
},
"banned": {
"tone": "danger",
"title": "Account status changed",
"subtitle": "Your account has been restricted or removed.",
"primary_label": "How it works",
"primary_url_key": "how_it_works_url",
"secondary_label": "",
"secondary_url_key": "",
},
}
DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"invited": {
"subject": "{{app_name}} invite for {{recipient_email}}",
@@ -81,18 +153,34 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"Build: {{build_number}}\n"
),
"body_html": (
"<h1>You have been invited</h1>"
"<p>You have been invited to <strong>{{app_name}}</strong>.</p>"
"<p><strong>Invite code:</strong> {{invite_code}}<br />"
"<strong>Invited by:</strong> {{inviter_username}}<br />"
"<strong>Invite label:</strong> {{invite_label}}<br />"
"<strong>Expires:</strong> {{invite_expires_at}}<br />"
"<strong>Remaining uses:</strong> {{invite_remaining_uses}}</p>"
"<p>{{invite_description}}</p>"
"<p>{{message}}</p>"
"<p><a href=\"{{invite_link}}\">Accept invite and create account</a></p>"
"<p><a href=\"{{how_it_works_url}}\">How it works</a></p>"
"<p class=\"meta\">Build {{build_number}}</p>"
"<div style=\"margin:0 0 20px; color:#e9ecf5; font-size:15px; line-height:1.7;\">"
"A new invitation has been prepared for <strong>{{recipient_email}}</strong>. Use the details below to sign up."
"</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:separate; border-spacing:10px 10px; margin:0 0 18px;\">"
"<tr>"
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Invite code</div>"
"<div style=\"font-size:24px; font-weight:800; letter-spacing:0.06em;\">{{invite_code}}</div>"
"</td>"
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Invited by</div>"
"<div style=\"font-size:20px; font-weight:700;\">{{inviter_username}}</div>"
"</td>"
"</tr>"
"<tr>"
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Invite label</div>"
"<div style=\"font-size:18px; font-weight:700;\">{{invite_label}}</div>"
"</td>"
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Access window</div>"
"<div style=\"font-size:16px; font-weight:700;\">{{invite_expires_at}}</div>"
"<div style=\"margin-top:6px; font-size:13px; color:#9aa3b8;\">Remaining uses: {{invite_remaining_uses}}</div>"
"</td>"
"</tr>"
"</table>"
"<div style=\"margin:0 0 18px; padding:18px; background:#101726; border:1px solid rgba(59,130,246,0.22); border-radius:18px; color:#dbe5ff; font-size:14px; line-height:1.7; white-space:pre-line;\">{{invite_description}}</div>"
"<div style=\"margin:0; padding:18px; background:#101726; border:1px dashed rgba(255,107,43,0.38); border-radius:18px; color:#dbe5ff; font-size:14px; line-height:1.7; white-space:pre-line;\">{{message}}</div>"
),
},
"welcome": {
@@ -106,12 +194,30 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"{{message}}\n"
),
"body_html": (
"<h1>Welcome</h1>"
"<p>Your {{app_name}} account is ready, <strong>{{username}}</strong>.</p>"
"<p><strong>Role:</strong> {{role}}</p>"
"<p><a href=\"{{app_url}}\">Open {{app_name}}</a><br />"
"<a href=\"{{how_it_works_url}}\">Read how it works</a></p>"
"<p>{{message}}</p>"
"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">"
"Your account is live and ready to use. Everything below mirrors the current site behavior."
"</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:separate; border-spacing:10px 10px; margin:0 0 18px;\">"
"<tr>"
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Username</div>"
"<div style=\"font-size:22px; font-weight:800;\">{{username}}</div>"
"</td>"
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Role</div>"
"<div style=\"font-size:22px; font-weight:800;\">{{role}}</div>"
"</td>"
"</tr>"
"</table>"
"<div style=\"margin:0 0 18px; padding:18px; background:#101726; border:1px solid rgba(34,197,94,0.24); border-radius:18px; color:#dbe5ff;\">"
"<div style=\"font-size:15px; font-weight:700; margin:0 0 10px;\">What to do next</div>"
"<ol style=\"margin:0; padding-left:20px; color:#dbe5ff; line-height:1.8; font-size:14px;\">"
"<li>Open Magent and sign in using your shared credentials.</li>"
"<li>Search or review requests without refreshing every page.</li>"
"<li>Use the invite tools in your profile if your account allows it.</li>"
"</ol>"
"</div>"
"<div style=\"margin:0; padding:18px; background:#101726; border:1px dashed rgba(59,130,246,0.32); border-radius:18px; color:#dbe5ff; font-size:14px; line-height:1.7; white-space:pre-line;\">{{message}}</div>"
),
},
"warning": {
@@ -124,12 +230,15 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"If you need help, contact the admin.\n"
),
"body_html": (
"<h1>Account warning</h1>"
"<p>Hello <strong>{{username}}</strong>,</p>"
"<p>This is a warning regarding your {{app_name}} account.</p>"
"<p><strong>Reason:</strong> {{reason}}</p>"
"<p>{{message}}</p>"
"<p>If you need help, contact the admin.</p>"
"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">"
"Please review this account notice carefully. This message was sent by an administrator."
"</div>"
"<div style=\"margin:0 0 18px; padding:18px; background:#241814; border:1px solid rgba(251,146,60,0.34); border-radius:18px; color:#ffe0ba;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#fbbd7b; margin-bottom:8px;\">Reason</div>"
"<div style=\"font-size:18px; font-weight:800; line-height:1.5; white-space:pre-line;\">{{reason}}</div>"
"</div>"
"<div style=\"margin:0 0 18px; padding:18px; background:#101726; border:1px solid rgba(255,255,255,0.08); border-radius:18px; color:#dbe5ff; font-size:14px; line-height:1.8; white-space:pre-line;\">{{message}}</div>"
"<div style=\"margin:0; color:#9aa3b8; font-size:13px; line-height:1.7;\">If you need help or think this was sent in error, contact the site administrator.</div>"
),
},
"banned": {
@@ -141,11 +250,18 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"{{message}}\n"
),
"body_html": (
"<h1>Account status changed</h1>"
"<p>Hello <strong>{{username}}</strong>,</p>"
"<p>Your {{app_name}} account has been banned or removed.</p>"
"<p><strong>Reason:</strong> {{reason}}</p>"
"<p>{{message}}</p>"
"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">"
"Your account access has changed. Review the details below."
"</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"margin:0 0 18px; border-collapse:collapse;\">"
"<tr>"
"<td style=\"padding:18px; background:#251418; border:1px solid rgba(239,68,68,0.32); border-radius:18px; color:#ffd0d0;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#ff9b9b; margin-bottom:8px;\">Reason</div>"
"<div style=\"font-size:18px; font-weight:800; line-height:1.5; white-space:pre-line;\">{{reason}}</div>"
"</td>"
"</tr>"
"</table>"
"<div style=\"margin:0; padding:18px; background:#101726; border:1px solid rgba(255,255,255,0.08); border-radius:18px; color:#dbe5ff; font-size:14px; line-height:1.8; white-space:pre-line;\">{{message}}</div>"
),
},
}
@@ -227,6 +343,108 @@ def _build_default_base_url() -> str:
return f"http://localhost:{port}"
def _looks_like_full_html_document(value: str) -> bool:
probe = value.lstrip().lower()
return probe.startswith("<!doctype") or probe.startswith("<html") or "<body" in probe[:300]
def _build_email_action_button(label: str, url: str, *, primary: bool) -> str:
background = "linear-gradient(135deg, #ff6b2b 0%, #1c6bff 100%)" if primary else "#151c2d"
border = "1px solid rgba(59, 130, 246, 0.32)" if primary else "1px solid rgba(255, 255, 255, 0.12)"
color = "#ffffff"
return (
f"<a href=\"{html.escape(url)}\" "
f"style=\"display:inline-block; padding:12px 20px; margin:0 12px 12px 0; border-radius:999px; "
f"background:{background}; border:{border}; color:{color}; text-decoration:none; font-size:14px; "
f"font-weight:800; letter-spacing:0.01em;\">{html.escape(label)}</a>"
)
def _wrap_email_html(
*,
app_name: str,
app_url: str,
build_number: str,
title: str,
subtitle: str,
tone: str,
body_html: str,
primary_label: str = "",
primary_url: str = "",
secondary_label: str = "",
secondary_url: str = "",
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))
if secondary_label and secondary_url:
actions.append(_build_email_action_button(secondary_label, secondary_url, primary=False))
actions_html = "".join(actions)
footer = footer_note or "This email was generated automatically by Magent."
logo_block = (
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 (
"<!doctype html>"
"<html><body style=\"margin:0; padding:0; background:#05070d;\">"
"<div style=\"display:none; max-height:0; overflow:hidden; opacity:0;\">"
f"{html.escape(title)} - {html.escape(subtitle)}"
"</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
"style=\"width:100%; border-collapse:collapse; background:radial-gradient(circle at top, rgba(17,33,74,0.9) 0%, rgba(8,12,22,1) 55%, #05070d 100%);\">"
"<tr><td style=\"padding:32px 18px;\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
"style=\"max-width:680px; margin:0 auto; border-collapse:collapse;\">"
"<tr><td style=\"padding:0 0 18px;\">"
f"<div style=\"padding:24px 28px; background:#0b0f18; border:1px solid rgba(255,255,255,0.08); border-radius:28px; box-shadow:0 24px 60px rgba(0,0,0,0.42);\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;\">"
"<tr>"
f"<td style=\"vertical-align:middle; width:64px; padding:0 18px 0 0;\">{logo_block}</td>"
"<td style=\"vertical-align:middle;\">"
f"<div style=\"font-size:11px; letter-spacing:0.18em; text-transform:uppercase; color:#9aa3b8; margin-bottom:6px;\">{html.escape(app_name)}</div>"
f"<div style=\"font-size:30px; line-height:1.1; font-weight:900; color:#e9ecf5; margin:0 0 6px;\">{html.escape(title)}</div>"
f"<div style=\"font-size:15px; line-height:1.6; color:#9aa3b8;\">{html.escape(subtitle or EMAIL_TAGLINE)}</div>"
"</td>"
"</tr>"
"</table>"
f"<div style=\"height:6px; margin:22px 0 22px; border-radius:999px; background:linear-gradient(90deg, {styles['accent_a']} 0%, {styles['accent_b']} 100%);\"></div>"
f"<div style=\"display:inline-block; padding:7px 12px; margin:0 0 16px; background:{styles['chip_bg']}; "
f"border:1px solid {styles['chip_border']}; border-radius:999px; color:{styles['chip_text']}; "
"font-size:11px; font-weight:800; letter-spacing:0.14em; text-transform:uppercase;\">"
f"{html.escape(EMAIL_TAGLINE)}</div>"
f"<div style=\"color:#e9ecf5;\">{body_html}</div>"
f"<div style=\"margin:24px 0 0;\">{actions_html}</div>"
"<div style=\"margin:28px 0 0; padding:18px 0 0; border-top:1px solid rgba(255,255,255,0.08);\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;\">"
"<tr>"
f"<td style=\"font-size:12px; line-height:1.7; color:#9aa3b8;\">{html.escape(footer)}</td>"
f"<td style=\"font-size:12px; line-height:1.7; color:#9aa3b8; text-align:right;\">Build {html.escape(build_number)}</td>"
"</tr>"
"</table>"
"</div>"
"</div>"
"</td></tr></table>"
"</td></tr></table>"
"</body></html>"
)
def build_invite_email_context(
*,
invite: Optional[Dict[str, Any]] = None,
@@ -348,11 +566,35 @@ def render_invite_email_template(
reason=reason,
overrides=overrides,
)
body_html = _render_template_string(template["body_html"], context, escape_html=True)
raw_body_html = _render_template_string(template["body_html"], context, escape_html=True)
body_text = _render_template_string(template["body_text"], context, escape_html=False)
if not body_text.strip() and body_html.strip():
body_text = _strip_html_for_text(body_html)
if not body_text.strip() and raw_body_html.strip():
body_text = _strip_html_for_text(raw_body_html)
subject = _render_template_string(template["subject"], context, escape_html=False)
presentation = TEMPLATE_PRESENTATION.get(template_key, TEMPLATE_PRESENTATION["invited"])
primary_url = _normalize_display_text(context.get(presentation["primary_url_key"], ""))
secondary_url = _normalize_display_text(context.get(presentation["secondary_url_key"], ""))
if _looks_like_full_html_document(raw_body_html):
body_html = raw_body_html.strip()
else:
body_html = _wrap_email_html(
app_name=_normalize_display_text(context.get("app_name"), env_settings.app_name),
app_url=_normalize_display_text(context.get("app_url"), _build_default_base_url()),
build_number=_normalize_display_text(context.get("build_number"), BUILD_NUMBER),
title=_normalize_display_text(context.get("title"), presentation["title"]),
subtitle=_normalize_display_text(context.get("subtitle"), presentation["subtitle"]),
tone=_normalize_display_text(context.get("tone"), presentation["tone"]),
body_html=raw_body_html.strip(),
primary_label=_normalize_display_text(
context.get("primary_label"), presentation["primary_label"]
),
primary_url=primary_url,
secondary_label=_normalize_display_text(
context.get("secondary_label"), presentation["secondary_label"]
),
secondary_url=secondary_url,
footer_note=_normalize_display_text(context.get("footer_note"), ""),
).strip()
return {
"subject": subject.strip(),
"body_text": body_text.strip(),
@@ -592,17 +834,43 @@ async def send_test_email(recipient_email: Optional[str] = None) -> Dict[str, st
raise RuntimeError("No valid recipient email is configured for the test message.")
application_url = _normalize_display_text(runtime.magent_application_url, "Not configured")
primary_url = application_url if application_url.lower().startswith(("http://", "https://")) else ""
subject = f"{env_settings.app_name} email test"
body_text = (
f"This is a test email from {env_settings.app_name}.\n\n"
f"Build: {BUILD_NUMBER}\n"
f"Application URL: {application_url}\n"
)
body_html = (
f"<h1>{html.escape(env_settings.app_name)} email test</h1>"
f"<p>This is a test email from <strong>{html.escape(env_settings.app_name)}</strong>.</p>"
f"<p><strong>Build:</strong> {html.escape(BUILD_NUMBER)}<br />"
f"<strong>Application URL:</strong> {html.escape(application_url)}</p>"
body_html = _wrap_email_html(
app_name=env_settings.app_name,
app_url=_build_default_base_url(),
build_number=BUILD_NUMBER,
title="Email delivery test",
subtitle="This confirms Magent can generate and hand off branded mail.",
tone="brand",
body_html=(
"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">"
"This is a live test email from Magent. If this renders correctly, the HTML template shell and SMTP handoff are both working."
"</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:separate; border-spacing:10px 10px; margin:0 0 18px;\">"
"<tr>"
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Build</div>"
f"<div style=\"font-size:22px; font-weight:800;\">{html.escape(BUILD_NUMBER)}</div>"
"</td>"
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Application URL</div>"
f"<div style=\"font-size:16px; font-weight:700; line-height:1.5;\">{html.escape(application_url)}</div>"
"</td>"
"</tr>"
"</table>"
"<div style=\"margin:0; padding:18px; background:#101726; border:1px dashed rgba(59,130,246,0.32); border-radius:18px; color:#dbe5ff; font-size:14px; line-height:1.7;\">"
"Use this test when changing SMTP settings, relay targets, or branding."
"</div>"
),
primary_label="Open Magent" if primary_url else "",
primary_url=primary_url,
footer_note="SMTP test email generated by Magent.",
)
receipt = await asyncio.to_thread(
@@ -654,13 +922,41 @@ async def send_password_reset_email(
f"Expires: {expires_at}\n\n"
"If you did not request this reset, you can ignore this email.\n"
)
body_html = (
f"<h1>{html.escape(env_settings.app_name)} password reset</h1>"
f"<p>A password reset was requested for <strong>{html.escape(username)}</strong>.</p>"
f"<p>This link will reset the password used for <strong>{html.escape(provider_label)}</strong>.</p>"
f"<p><a href=\"{html.escape(reset_url)}\">Reset password</a></p>"
f"<p><strong>Expires:</strong> {html.escape(expires_at)}</p>"
"<p>If you did not request this reset, you can ignore this email.</p>"
body_html = _wrap_email_html(
app_name=env_settings.app_name,
app_url=app_url,
build_number=BUILD_NUMBER,
title="Reset your password",
subtitle=f"This will update the credentials used for {provider_label}.",
tone="brand",
body_html=(
f"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">"
f"A password reset was requested for <strong>{html.escape(username)}</strong>."
"</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:separate; border-spacing:10px 10px; margin:0 0 18px;\">"
"<tr>"
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Account</div>"
f"<div style=\"font-size:22px; font-weight:800;\">{html.escape(username)}</div>"
"</td>"
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">"
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Expires</div>"
f"<div style=\"font-size:16px; font-weight:700; line-height:1.5;\">{html.escape(expires_at)}</div>"
"</td>"
"</tr>"
"</table>"
f"<div style=\"margin:0 0 18px; padding:18px; background:#101726; border:1px solid rgba(59,130,246,0.22); border-radius:18px; color:#dbe5ff; font-size:14px; line-height:1.7;\">"
f"This reset will update the password used for <strong>{html.escape(provider_label)}</strong>."
"</div>"
"<div style=\"margin:0; padding:18px; background:#1a1220; border:1px dashed rgba(255,107,43,0.38); border-radius:18px; color:#ffd3bf; font-size:14px; line-height:1.7;\">"
"If you did not request this reset, ignore this email. No changes will be applied until the reset link is opened and completed."
"</div>"
),
primary_label="Reset password",
primary_url=reset_url,
secondary_label="Open Magent",
secondary_url=app_url,
footer_note="Password reset email generated by Magent.",
)
receipt = await asyncio.to_thread(

View File

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

View File

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