Compare commits

..

3 Commits

6 changed files with 481 additions and 148 deletions

View File

@@ -1 +1 @@
0303261629 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": {
@@ -136,6 +140,111 @@ TEMPLATE_PRESENTATION: Dict[str, Dict[str, str]] = {
}, },
} }
def _build_email_stat_card(label: str, value: str, detail: str = "") -> str:
detail_html = (
f"<div style=\"margin-top:8px; font-size:13px; line-height:1.6; color:#5c687d; word-break:break-word;\">"
f"{html.escape(detail)}</div>"
if detail
else ""
)
return (
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
"style=\"border-collapse:separate; background:#f8fafc; border:1px solid #d9e2ef; border-radius:16px;\">"
"<tr><td style=\"padding:16px; font-family:Segoe UI, Arial, sans-serif; color:#132033;\">"
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"{html.escape(value)}</div>"
f"{detail_html}"
"</td></tr></table>"
)
def _build_email_stat_grid(cards: list[str]) -> str:
if not cards:
return ""
rows: list[str] = []
for index in range(0, len(cards), 2):
left = cards[index]
right = cards[index + 1] if index + 1 < len(cards) else ""
rows.append(
"<tr>"
f"<td width=\"50%\" style=\"vertical-align:top; padding:0 5px 10px 0;\">{left}</td>"
f"<td width=\"50%\" style=\"vertical-align:top; padding:0 0 10px 5px;\">{right}</td>"
"</tr>"
)
return (
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
"style=\"border-collapse:collapse; margin:0 0 18px;\">"
f"{''.join(rows)}"
"</table>"
)
def _build_email_list(items: list[str], *, ordered: bool = False) -> str:
tag = "ol" if ordered else "ul"
marker = "padding-left:20px;" if ordered else "padding-left:18px;"
rendered_items = "".join(
f"<li style=\"margin:0 0 8px;\">{html.escape(item)}</li>" for item in items if item
)
return (
f"<{tag} style=\"margin:0; {marker} color:#132033; line-height:1.8; font-size:14px;\">"
f"{rendered_items}"
f"</{tag}>"
)
def _build_email_panel(title: str, body_html: str, *, variant: str = "neutral") -> str:
styles = {
"neutral": {
"background": "#f8fafc",
"border": "#d9e2ef",
"eyebrow": "#6b778c",
"text": "#132033",
},
"brand": {
"background": "#eef4ff",
"border": "#bfd2ff",
"eyebrow": "#2754b6",
"text": "#132033",
},
"success": {
"background": "#edf9f0",
"border": "#bfe4c6",
"eyebrow": "#1f7a3f",
"text": "#132033",
},
"warning": {
"background": "#fff5ea",
"border": "#ffd5a8",
"eyebrow": "#c46a10",
"text": "#132033",
},
"danger": {
"background": "#fff0f0",
"border": "#f3c1c1",
"eyebrow": "#bb2d2d",
"text": "#132033",
},
}.get(variant, {
"background": "#f8fafc",
"border": "#d9e2ef",
"eyebrow": "#6b778c",
"text": "#132033",
})
return (
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
f"style=\"border-collapse:separate; margin:0 0 18px; background:{styles['background']}; "
f"border:1px solid {styles['border']}; border-radius:18px;\">"
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>"
"</td></tr></table>"
)
DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = { DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"invited": { "invited": {
"subject": "{{app_name}} invite for {{recipient_email}}", "subject": "{{app_name}} invite for {{recipient_email}}",
@@ -153,34 +262,43 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"Build: {{build_number}}\n" "Build: {{build_number}}\n"
), ),
"body_html": ( "body_html": (
"<div style=\"margin:0 0 20px; color:#e9ecf5; font-size:15px; line-height:1.7;\">" "<div style=\"margin:0 0 20px; color:#132033; 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." "A new invitation has been prepared for <strong>{{recipient_email}}</strong>. Use the details below to sign up."
"</div>" "</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:separate; border-spacing:10px 10px; margin:0 0 18px;\">" + _build_email_stat_grid(
"<tr>" [
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">" _build_email_stat_card("Invite code", "{{invite_code}}"),
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Invite code</div>" _build_email_stat_card("Invited by", "{{inviter_username}}"),
"<div style=\"font-size:24px; font-weight:800; letter-spacing:0.06em;\">{{invite_code}}</div>" _build_email_stat_card("Invite label", "{{invite_label}}"),
"</td>" _build_email_stat_card(
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">" "Access window",
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Invited by</div>" "{{invite_expires_at}}",
"<div style=\"font-size:20px; font-weight:700;\">{{inviter_username}}</div>" "Remaining uses: {{invite_remaining_uses}}",
"</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;\">" + _build_email_panel(
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Invite label</div>" "Invitation details",
"<div style=\"font-size:18px; font-weight:700;\">{{invite_label}}</div>" "<div style=\"white-space:pre-line;\">{{invite_description}}</div>",
"</td>" variant="brand",
"<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>" + _build_email_panel(
"<div style=\"font-size:16px; font-weight:700;\">{{invite_expires_at}}</div>" "Message from admin",
"<div style=\"margin-top:6px; font-size:13px; color:#9aa3b8;\">Remaining uses: {{invite_remaining_uses}}</div>" "<div style=\"white-space:pre-line;\">{{message}}</div>",
"</td>" variant="neutral",
"</tr>" )
"</table>" + _build_email_panel(
"<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>" "What happens next",
"<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>" _build_email_list(
[
"Open the invite link and complete the signup flow.",
"Sign in using the shared credentials for Magent and Seerr.",
"Use the How it works page if you want a quick overview first.",
],
ordered=True,
),
variant="neutral",
)
), ),
}, },
"welcome": { "welcome": {
@@ -194,30 +312,34 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"{{message}}\n" "{{message}}\n"
), ),
"body_html": ( "body_html": (
"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">" "<div style=\"margin:0 0 18px; color:#132033; font-size:15px; line-height:1.7;\">"
"Your account is live and ready to use. Everything below mirrors the current site behavior." "Your account is live and ready to use. Everything below mirrors the current site behavior."
"</div>" "</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:separate; border-spacing:10px 10px; margin:0 0 18px;\">" + _build_email_stat_grid(
"<tr>" [
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">" _build_email_stat_card("Username", "{{username}}"),
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Username</div>" _build_email_stat_card("Role", "{{role}}"),
"<div style=\"font-size:22px; font-weight:800;\">{{username}}</div>" _build_email_stat_card("Magent", "{{app_url}}"),
"</td>" _build_email_stat_card("Guides", "{{how_it_works_url}}"),
"<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>" + _build_email_panel(
"</td>" "What to do next",
"</tr>" _build_email_list(
"</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;\">" "Open Magent and sign in using your shared credentials.",
"<div style=\"font-size:15px; font-weight:700; margin:0 0 10px;\">What to do next</div>" "Search all requests or review your own activity without refreshing the page.",
"<ol style=\"margin:0; padding-left:20px; color:#dbe5ff; line-height:1.8; font-size:14px;\">" "Use the invite tools in your profile if your account allows it.",
"<li>Open Magent and sign in using your shared credentials.</li>" ],
"<li>Search or review requests without refreshing every page.</li>" ordered=True,
"<li>Use the invite tools in your profile if your account allows it.</li>" ),
"</ol>" variant="success",
"</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>" + _build_email_panel(
"Additional notes",
"<div style=\"white-space:pre-line;\">{{message}}</div>",
variant="neutral",
)
), ),
}, },
"warning": { "warning": {
@@ -230,15 +352,38 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"If you need help, contact the admin.\n" "If you need help, contact the admin.\n"
), ),
"body_html": ( "body_html": (
"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">" "<div style=\"margin:0 0 18px; color:#132033; font-size:15px; line-height:1.7;\">"
"Please review this account notice carefully. This message was sent by an administrator." "Please review this account notice carefully. This message was sent by an administrator."
"</div>" "</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;\">" + _build_email_stat_grid(
"<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>" _build_email_stat_card("Account", "{{username}}"),
"</div>" _build_email_stat_card("Role", "{{role}}"),
"<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>" _build_email_stat_card("Application", "{{app_name}}"),
"<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>" _build_email_stat_card("Support", "{{how_it_works_url}}"),
]
)
+ _build_email_panel(
"Reason",
"<div style=\"font-size:18px; font-weight:800; line-height:1.6; white-space:pre-line;\">{{reason}}</div>",
variant="warning",
)
+ _build_email_panel(
"Administrator note",
"<div style=\"white-space:pre-line;\">{{message}}</div>",
variant="neutral",
)
+ _build_email_panel(
"What to do next",
_build_email_list(
[
"Review the note above and confirm you understand what needs to change.",
"If you need help, reply through your usual support path or contact an administrator.",
"Keep this email for reference until the matter is resolved.",
]
),
variant="neutral",
)
), ),
}, },
"banned": { "banned": {
@@ -250,18 +395,38 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = {
"{{message}}\n" "{{message}}\n"
), ),
"body_html": ( "body_html": (
"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">" "<div style=\"margin:0 0 18px; color:#132033; font-size:15px; line-height:1.7;\">"
"Your account access has changed. Review the details below." "Your account access has changed. Review the details below."
"</div>" "</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"margin:0 0 18px; border-collapse:collapse;\">" + _build_email_stat_grid(
"<tr>" [
"<td style=\"padding:18px; background:#251418; border:1px solid rgba(239,68,68,0.32); border-radius:18px; color:#ffd0d0;\">" _build_email_stat_card("Account", "{{username}}"),
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#ff9b9b; margin-bottom:8px;\">Reason</div>" _build_email_stat_card("Status", "Restricted"),
"<div style=\"font-size:18px; font-weight:800; line-height:1.5; white-space:pre-line;\">{{reason}}</div>" _build_email_stat_card("Application", "{{app_name}}"),
"</td>" _build_email_stat_card("Guidance", "{{how_it_works_url}}"),
"</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>" + _build_email_panel(
"Reason",
"<div style=\"font-size:18px; font-weight:800; line-height:1.6; white-space:pre-line;\">{{reason}}</div>",
variant="danger",
)
+ _build_email_panel(
"Administrator note",
"<div style=\"white-space:pre-line;\">{{message}}</div>",
variant="neutral",
)
+ _build_email_panel(
"What this means",
_build_email_list(
[
"Your access has been removed or restricted across the linked services.",
"If you believe this is incorrect, contact the site administrator directly.",
"Do not rely on old links or cached sessions after this change.",
]
),
variant="neutral",
)
), ),
}, },
} }
@@ -349,17 +514,141 @@ def _looks_like_full_html_document(value: str) -> bool:
def _build_email_action_button(label: str, url: str, *, primary: bool) -> str: def _build_email_action_button(label: str, url: str, *, primary: bool) -> str:
background = "linear-gradient(135deg, #ff6b2b 0%, #1c6bff 100%)" if primary else "#151c2d" background = "linear-gradient(135deg, #ff6b2b 0%, #1c6bff 100%)" if primary else "#ffffff"
border = "1px solid rgba(59, 130, 246, 0.32)" if primary else "1px solid rgba(255, 255, 255, 0.12)" fallback = "#1c6bff" if primary else "#ffffff"
color = "#ffffff" border = "1px solid rgba(28, 107, 255, 0.28)" if primary else "1px solid #d5deed"
color = "#ffffff" if primary else "#132033"
return ( return (
f"<a href=\"{html.escape(url)}\" " f"<a href=\"{html.escape(url)}\" "
f"style=\"display:inline-block; padding:12px 20px; margin:0 12px 12px 0; border-radius:999px; " 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"background-color:{fallback}; background:{background}; border:{border}; color:{color}; text-decoration:none; font-size:14px; "
f"font-weight:800; letter-spacing:0.01em;\">{html.escape(label)}</a>" f"font-weight:800; letter-spacing:0.01em;\">{html.escape(label)}</a>"
) )
@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,
@@ -376,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))
@@ -388,53 +673,43 @@ 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>"
"<html><body style=\"margin:0; padding:0; background:#05070d;\">" "<html><body style=\"margin:0; padding:0; background:#eef2f7;\" bgcolor=\"#eef2f7\">"
"<div style=\"display:none; max-height:0; overflow:hidden; opacity:0;\">" "<div style=\"display:none; max-height:0; overflow:hidden; opacity:0;\">"
f"{html.escape(title)} - {html.escape(subtitle)}" f"{html.escape(title)} - {html.escape(subtitle)}"
"</div>" "</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" " "<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%);\">" "style=\"width:100%; border-collapse:collapse; background:#eef2f7;\" bgcolor=\"#eef2f7\">"
"<tr><td style=\"padding:32px 18px;\">" "<tr><td style=\"padding:32px 18px;\" bgcolor=\"#eef2f7\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" " "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" "
"style=\"max-width:680px; margin:0 auto; border-collapse:collapse;\">" "style=\"max-width:680px; margin:0 auto; border-collapse:collapse;\">"
"<tr><td style=\"padding:0 0 18px;\">" "<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);\">" f"<div style=\"padding:24px 28px; background:#ffffff; border:1px solid #d5deed; border-radius:28px; box-shadow:0 18px 48px rgba(15,23,42,0.08);\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;\">" "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;\">"
"<tr>" "<tr>"
f"<td style=\"vertical-align:middle; width:64px; padding:0 18px 0 0;\">{logo_block}</td>" f"<td style=\"vertical-align:middle; width:64px; padding:0 18px 0 0;\">{logo_block}</td>"
"<td style=\"vertical-align:middle;\">" "<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:11px; letter-spacing:0.18em; text-transform:uppercase; color:#6b778c; 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:30px; line-height:1.1; font-weight:900; color:#132033; 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>" f"<div style=\"font-size:15px; line-height:1.6; color:#5c687d;\">{html.escape(subtitle or EMAIL_TAGLINE)}</div>"
"</td>" "</td>"
"</tr>" "</tr>"
"</table>" "</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=\"height:6px; margin:22px 0 22px; border-radius:999px; background-color:{styles['accent_b']}; 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"<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']}; " 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;\">" "font-size:11px; font-weight:800; letter-spacing:0.14em; text-transform:uppercase;\">"
f"{html.escape(EMAIL_TAGLINE)}</div>" f"{html.escape(EMAIL_TAGLINE)}</div>"
f"<div style=\"color:#e9ecf5;\">{body_html}</div>" f"<div style=\"color:#132033;\">{body_html}</div>"
f"<div style=\"margin:24px 0 0;\">{actions_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);\">" "<div style=\"margin:28px 0 0; padding:18px 0 0; border-top:1px solid #e2e8f0;\">"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;\">" "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;\">"
"<tr>" "<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:#6b778c;\">{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>" f"<td style=\"font-size:12px; line-height:1.7; color:#6b778c; text-align:right;\">Build {html.escape(build_number)}</td>"
"</tr>" "</tr>"
"</table>" "</table>"
"</div>" "</div>"
@@ -481,7 +756,7 @@ def build_invite_email_context(
invite.get("created_by") if invite else (user.get("username") if user else None), invite.get("created_by") if invite else (user.get("username") if user else None),
"Admin", "Admin",
), ),
"message": _normalize_display_text(message, ""), "message": _normalize_display_text(message, "No additional note."),
"reason": _normalize_display_text(reason, "Not specified"), "reason": _normalize_display_text(reason, "Not specified"),
"recipient_email": _normalize_display_text(resolved_recipient, "No email supplied"), "recipient_email": _normalize_display_text(resolved_recipient, "No email supplied"),
"role": _normalize_display_text(user.get("role") if user else None, "user"), "role": _normalize_display_text(user.get("role") if user else None, "user"),
@@ -644,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()
@@ -722,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:
@@ -835,11 +1128,24 @@ async def send_test_email(recipient_email: Optional[str] = None) -> Dict[str, st
application_url = _normalize_display_text(runtime.magent_application_url, "Not configured") application_url = _normalize_display_text(runtime.magent_application_url, "Not configured")
primary_url = application_url if application_url.lower().startswith(("http://", "https://")) else "" primary_url = application_url if application_url.lower().startswith(("http://", "https://")) else ""
smtp_target = f"{_normalize_display_text(runtime.magent_notify_email_smtp_host, 'Not configured')}:{int(runtime.magent_notify_email_smtp_port or 587)}"
security_mode = "SSL" if runtime.magent_notify_email_use_ssl else ("STARTTLS" if runtime.magent_notify_email_use_tls else "Plain SMTP")
auth_mode = "Authenticated" if (
_normalize_display_text(runtime.magent_notify_email_smtp_username)
and _normalize_display_text(runtime.magent_notify_email_smtp_password)
) else "No SMTP auth"
delivery_warning = smtp_email_delivery_warning()
subject = f"{env_settings.app_name} email test" subject = f"{env_settings.app_name} email test"
body_text = ( body_text = (
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,
@@ -849,24 +1155,39 @@ async def send_test_email(recipient_email: Optional[str] = None) -> Dict[str, st
subtitle="This confirms Magent can generate and hand off branded mail.", subtitle="This confirms Magent can generate and hand off branded mail.",
tone="brand", tone="brand",
body_html=( body_html=(
"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">" "<div style=\"margin:0 0 18px; color:#132033; 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." "This is a live test email from Magent. If this renders correctly, the HTML template shell and SMTP handoff are both working."
"</div>" "</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:separate; border-spacing:10px 10px; margin:0 0 18px;\">" + _build_email_stat_grid(
"<tr>" [
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">" _build_email_stat_card("Recipient", resolved_email),
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Build</div>" _build_email_stat_card("Build", BUILD_NUMBER),
f"<div style=\"font-size:22px; font-weight:800;\">{html.escape(BUILD_NUMBER)}</div>" _build_email_stat_card("SMTP target", smtp_target),
"</td>" _build_email_stat_card("Security", security_mode, auth_mode),
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">" _build_email_stat_card("Application URL", application_url),
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Application URL</div>" _build_email_stat_card("Template shell", "Branded HTML", "Logo, gradient, action buttons"),
f"<div style=\"font-size:16px; font-weight:700; line-height:1.5;\">{html.escape(application_url)}</div>" ]
"</td>" )
"</tr>" + _build_email_panel(
"</table>" "What this verifies",
"<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;\">" _build_email_list(
"Use this test when changing SMTP settings, relay targets, or branding." [
"</div>" "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.",
]
),
variant="brand",
)
+ _build_email_panel(
"Delivery notes",
(
f"<div style=\"white-space:pre-line;\">{html.escape(delivery_warning)}</div>"
if delivery_warning
else "Use this test when changing SMTP settings, relay targets, or branding."
),
variant="warning" if delivery_warning else "neutral",
)
), ),
primary_label="Open Magent" if primary_url else "", primary_label="Open Magent" if primary_url else "",
primary_url=primary_url, primary_url=primary_url,
@@ -930,27 +1251,39 @@ async def send_password_reset_email(
subtitle=f"This will update the credentials used for {provider_label}.", subtitle=f"This will update the credentials used for {provider_label}.",
tone="brand", tone="brand",
body_html=( body_html=(
f"<div style=\"margin:0 0 18px; color:#e9ecf5; font-size:15px; line-height:1.7;\">" f"<div style=\"margin:0 0 18px; color:#132033; font-size:15px; line-height:1.7;\">"
f"A password reset was requested for <strong>{html.escape(username)}</strong>." f"A password reset was requested for <strong>{html.escape(username)}</strong>."
"</div>" "</div>"
"<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:separate; border-spacing:10px 10px; margin:0 0 18px;\">" + _build_email_stat_grid(
"<tr>" [
"<td width=\"50%\" style=\"padding:16px; background:#151c2d; border:1px solid rgba(255,255,255,0.08); border-radius:16px; color:#e9ecf5;\">" _build_email_stat_card("Account", username),
"<div style=\"font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:#9aa3b8; margin-bottom:8px;\">Account</div>" _build_email_stat_card("Expires", expires_at),
f"<div style=\"font-size:22px; font-weight:800;\">{html.escape(username)}</div>" _build_email_stat_card("Credentials updated", provider_label),
"</td>" _build_email_stat_card("Delivery target", resolved_email),
"<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>" + _build_email_panel(
"</td>" "What will be updated",
"</tr>" f"This reset will update the password used for <strong>{html.escape(provider_label)}</strong>.",
"</table>" variant="brand",
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>." + _build_email_panel(
"</div>" "What happens next",
"<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;\">" _build_email_list(
"If you did not request this reset, ignore this email. No changes will be applied until the reset link is opened and completed." [
"</div>" "Open the reset link and choose a new password.",
"Complete the form before the expiry time shown above.",
"Use the new password the next time you sign in.",
],
ordered=True,
),
variant="neutral",
)
+ _build_email_panel(
"Safety note",
"If you did not request this reset, ignore this email. No changes will be applied until the reset link is opened and completed.",
variant="warning",
)
), ),
primary_label="Reset password", primary_label="Reset password",
primary_url=reset_url, primary_url=reset_url,

View File

@@ -2296,14 +2296,6 @@ export default function SettingsPage({ section }: SettingsPageProps) {
/> />
</label> </label>
) : null} ) : null}
<button
type="button"
className="settings-action-button"
onClick={() => void saveSettingGroup(sectionGroup)}
disabled={sectionSaving[sectionGroup.key] || sectionTesting[sectionGroup.key]}
>
{sectionSaving[sectionGroup.key] ? 'Saving...' : 'Save section'}
</button>
{getSectionTestLabel(sectionGroup.key) ? ( {getSectionTestLabel(sectionGroup.key) ? (
<button <button
type="button" type="button"
@@ -2316,6 +2308,14 @@ export default function SettingsPage({ section }: SettingsPageProps) {
: getSectionTestLabel(sectionGroup.key)} : getSectionTestLabel(sectionGroup.key)}
</button> </button>
) : null} ) : null}
<button
type="button"
className="settings-action-button"
onClick={() => void saveSettingGroup(sectionGroup)}
disabled={sectionSaving[sectionGroup.key] || sectionTesting[sectionGroup.key]}
>
{sectionSaving[sectionGroup.key] ? 'Saving...' : 'Save section'}
</button>
</div> </div>
</section> </section>
))} ))}

View File

@@ -1,12 +1,12 @@
{ {
"name": "magent-frontend", "name": "magent-frontend",
"version": "0303261629", "version": "0303261841",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "magent-frontend", "name": "magent-frontend",
"version": "0303261629", "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": "0303261629", "version": "0303261841",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",