Add branded HTML email templates
This commit is contained in:
@@ -1 +1 @@
|
||||
0303261611
|
||||
0303261629
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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(
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "magent-frontend",
|
||||
"private": true,
|
||||
"version": "0303261611",
|
||||
"version": "0303261629",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
||||
Reference in New Issue
Block a user