feat(email): контактный адрес инстанса, Reply-To в письмах и тестовая отправка
Новая настройка instance_settings.contact_email (включён/адрес, с валидацией формата) — подставляется в заголовок Reply-To писем подтверждения регистрации, приглашений и саммари. Админ-эндпоинт POST /admin/settings/test-email отправляет проверочное письмо синхронно и возвращает внятный результат (успех либо текст ошибки транспорта), не раскрывая логин/пароль SMTP.
This commit is contained in:
@@ -56,8 +56,14 @@ class EmailBackend(Protocol):
|
||||
body: str,
|
||||
html_body: str | None = None,
|
||||
attachments: Sequence[EmailAttachment] = (),
|
||||
reply_to: str | None = None,
|
||||
) -> None:
|
||||
"""Отправить письмо получателю `to` (plaintext body обязателен, HTML — альтернатива)."""
|
||||
"""Отправить письмо получателю `to` (plaintext body обязателен, HTML — альтернатива).
|
||||
|
||||
`reply_to` — необязательный контактный адрес инстанса (см.
|
||||
`services/instance_settings.py::InstanceConfig.contact_email`), проставляется
|
||||
заголовком `Reply-To`, если задан.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@@ -72,13 +78,15 @@ class ConsoleEmailBackend:
|
||||
body: str,
|
||||
html_body: str | None = None,
|
||||
attachments: Sequence[EmailAttachment] = (),
|
||||
reply_to: str | None = None,
|
||||
) -> None:
|
||||
"""Залогировать письмо (вложения — только имена файлов, без содержимого)."""
|
||||
attachment_names = ", ".join(a.filename for a in attachments) or "нет"
|
||||
logger.info(
|
||||
"EMAIL to=%s subject=%s attachments=[%s]\n%s",
|
||||
"EMAIL to=%s subject=%s reply_to=%s attachments=[%s]\n%s",
|
||||
to,
|
||||
subject,
|
||||
reply_to or "нет",
|
||||
attachment_names,
|
||||
body,
|
||||
)
|
||||
@@ -116,6 +124,7 @@ class SmtpEmailBackend:
|
||||
body: str,
|
||||
html_body: str | None = None,
|
||||
attachments: Sequence[EmailAttachment] = (),
|
||||
reply_to: str | None = None,
|
||||
) -> None:
|
||||
"""Отправить письмо; ошибки транспорта транслируются в `EmailSendError`."""
|
||||
message = _build_message(
|
||||
@@ -125,6 +134,7 @@ class SmtpEmailBackend:
|
||||
body=body,
|
||||
html_body=html_body,
|
||||
attachments=attachments,
|
||||
reply_to=reply_to,
|
||||
)
|
||||
try:
|
||||
await aiosmtplib.send(
|
||||
@@ -165,12 +175,15 @@ def _build_message(
|
||||
body: str,
|
||||
html_body: str | None,
|
||||
attachments: Sequence[EmailAttachment],
|
||||
reply_to: str | None = None,
|
||||
) -> EmailMessage:
|
||||
"""Собрать `EmailMessage`: plaintext (+ HTML-альтернатива) + вложения."""
|
||||
message = EmailMessage()
|
||||
message["From"] = sender
|
||||
message["To"] = to
|
||||
message["Subject"] = subject
|
||||
if reply_to:
|
||||
message["Reply-To"] = reply_to
|
||||
message.set_content(body)
|
||||
if html_body is not None:
|
||||
message.add_alternative(html_body, subtype="html")
|
||||
|
||||
Reference in New Issue
Block a user