|
|
|
@@ -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:
|
|
|
|
|