Compare commits

..

2 Commits

Author SHA1 Message Date
494b79ed26 Process 1 build 0403261902 2026-03-04 19:03:52 +13:00
d30a2473ce Improve email deliverability headers and SMTP identity 2026-03-04 17:37:51 +13:00
5 changed files with 55 additions and 11 deletions

View File

@@ -1 +1 @@
0403261321 0403261902

File diff suppressed because one or more lines are too long

View File

@@ -14,6 +14,7 @@ from email.policy import SMTP as SMTP_POLICY
from email.utils import formataddr, formatdate, make_msgid 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
from urllib.parse import urlparse
from ..build_info import BUILD_NUMBER from ..build_info import BUILD_NUMBER
from ..config import settings as env_settings from ..config import settings as env_settings
@@ -512,6 +513,40 @@ def _build_default_base_url() -> str:
return f"http://localhost:{port}" return f"http://localhost:{port}"
def _derive_mail_hostname(*, from_address: str) -> str:
runtime = get_runtime_settings()
candidates = (
runtime.magent_application_url,
runtime.magent_proxy_base_url,
env_settings.cors_allow_origin,
)
for candidate in candidates:
normalized = _normalize_display_text(candidate)
if not normalized:
continue
parsed = urlparse(normalized if "://" in normalized else f"https://{normalized}")
hostname = _normalize_display_text(parsed.hostname)
if hostname and "." in hostname:
return hostname
domain = _normalize_display_text(from_address.split("@", 1)[1] if "@" in from_address else None)
if domain and "." in domain:
return domain
return "localhost"
def _add_transactional_headers(
message: EmailMessage,
*,
from_name: str,
from_address: str,
) -> None:
message["Reply-To"] = formataddr((from_name, from_address))
message["Organization"] = env_settings.app_name
message["X-Mailer"] = f"{env_settings.app_name}/{BUILD_NUMBER}"
message["Auto-Submitted"] = "auto-generated"
message["X-Auto-Response-Suppress"] = "All"
def _looks_like_full_html_document(value: str) -> bool: def _looks_like_full_html_document(value: str) -> bool:
probe = value.lstrip().lower() probe = value.lstrip().lower()
return probe.startswith("<!doctype") or probe.startswith("<html") or "<body" in probe[:300] return probe.startswith("<!doctype") or probe.startswith("<html") or "<body" in probe[:300]
@@ -918,8 +953,10 @@ def smtp_email_delivery_warning() -> Optional[str]:
if host.endswith(".mail.protection.outlook.com") and not (username and password): if host.endswith(".mail.protection.outlook.com") and not (username and password):
return ( return (
"Unauthenticated Microsoft 365 relay mode is configured. SMTP acceptance does not " "Unauthenticated Microsoft 365 relay mode is configured. SMTP acceptance does not "
"confirm mailbox delivery. For reliable delivery, use smtp.office365.com:587 with " "confirm mailbox delivery, and suspicious messages can still be filtered. For reliable "
"SMTP credentials or configure a verified Exchange relay connector." "delivery, use smtp.office365.com:587 with SMTP credentials or configure a verified "
"Exchange relay connector and make sure SPF, DKIM, and DMARC are healthy for the "
"sender domain."
) )
return None return None
@@ -986,8 +1023,9 @@ def _send_email_sync(*, recipient_email: str, subject: str, body_text: str, body
delivery_warning = smtp_email_delivery_warning() delivery_warning = smtp_email_delivery_warning()
if not host or not from_address: if not host or not from_address:
raise RuntimeError("SMTP email settings are incomplete.") raise RuntimeError("SMTP email settings are incomplete.")
local_hostname = _derive_mail_hostname(from_address=from_address)
logger.info( logger.info(
"smtp send started recipient=%s from=%s host=%s port=%s tls=%s ssl=%s auth=%s subject=%s", "smtp send started recipient=%s from=%s host=%s port=%s tls=%s ssl=%s auth=%s subject=%s ehlo=%s",
recipient_email, recipient_email,
from_address, from_address,
host, host,
@@ -996,6 +1034,7 @@ def _send_email_sync(*, recipient_email: str, subject: str, body_text: str, body
use_ssl, use_ssl,
bool(username and password), bool(username and password),
subject, subject,
local_hostname,
) )
if delivery_warning: if delivery_warning:
logger.warning("smtp delivery warning host=%s detail=%s", host, delivery_warning) logger.warning("smtp delivery warning host=%s detail=%s", host, delivery_warning)
@@ -1009,6 +1048,11 @@ def _send_email_sync(*, recipient_email: str, subject: str, body_text: str, body
message["Message-ID"] = make_msgid(domain=from_address.split("@", 1)[1]) message["Message-ID"] = make_msgid(domain=from_address.split("@", 1)[1])
else: else:
message["Message-ID"] = make_msgid() message["Message-ID"] = make_msgid()
_add_transactional_headers(
message,
from_name=from_name,
from_address=from_address,
)
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")
@@ -1027,7 +1071,7 @@ def _send_email_sync(*, recipient_email: str, subject: str, body_text: str, body
) )
if use_ssl: if use_ssl:
with smtplib.SMTP_SSL(host, port, timeout=20) as smtp: with smtplib.SMTP_SSL(host, port, timeout=20, local_hostname=local_hostname) as smtp:
logger.debug("smtp ssl connection opened host=%s port=%s", host, port) logger.debug("smtp ssl connection opened host=%s port=%s", host, port)
if username and password: if username and password:
smtp.login(username, password) smtp.login(username, password)
@@ -1047,7 +1091,7 @@ def _send_email_sync(*, recipient_email: str, subject: str, body_text: str, body
) )
return receipt return receipt
with smtplib.SMTP(host, port, timeout=20) as smtp: with smtplib.SMTP(host, port, timeout=20, local_hostname=local_hostname) as smtp:
logger.debug("smtp connection opened host=%s port=%s", host, port) logger.debug("smtp connection opened host=%s port=%s", host, port)
smtp.ehlo() smtp.ehlo()
if use_tls: if use_tls:

View File

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