feat(email): контактный адрес инстанса, Reply-To в письмах и тестовая отправка
Новая настройка instance_settings.contact_email (включён/адрес, с валидацией формата) — подставляется в заголовок Reply-To писем подтверждения регистрации, приглашений и саммари. Админ-эндпоинт POST /admin/settings/test-email отправляет проверочное письмо синхронно и возвращает внятный результат (успех либо текст ошибки транспорта), не раскрывая логин/пароль SMTP.
This commit is contained in:
@@ -45,6 +45,8 @@ from schemas.admin import (
|
||||
TeamListOut,
|
||||
TeamOut,
|
||||
TeamUpdateIn,
|
||||
TestEmailIn,
|
||||
TestEmailOut,
|
||||
)
|
||||
from schemas.conferences import ConferenceUpdateIn
|
||||
from services.ai_levels import detect_ai_levels
|
||||
@@ -56,9 +58,11 @@ from services.conferences import (
|
||||
InvalidConferenceStateError,
|
||||
NotConferenceOwnerError,
|
||||
)
|
||||
from services.email import EmailSendError, create_email_backend
|
||||
from services.instance_settings import (
|
||||
InstanceSettingsService,
|
||||
InvalidAiLevelError,
|
||||
InvalidContactEmailError,
|
||||
InvalidEmailDomainError,
|
||||
InvalidTimezoneError,
|
||||
SettingsUpdateIn,
|
||||
@@ -391,12 +395,59 @@ async def update_settings(
|
||||
service = InstanceSettingsService(session)
|
||||
try:
|
||||
cfg = await service.update(data)
|
||||
except (InvalidAiLevelError, InvalidTimezoneError, InvalidEmailDomainError) as exc:
|
||||
except (
|
||||
InvalidAiLevelError,
|
||||
InvalidTimezoneError,
|
||||
InvalidEmailDomainError,
|
||||
InvalidContactEmailError,
|
||||
) as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
queue_served = await anyio.to_thread.run_sync(transcription_queue_served)
|
||||
return _to_settings_out(cfg, transcription_queue_served=queue_served)
|
||||
|
||||
|
||||
@router.post("/settings/test-email", response_model=TestEmailOut)
|
||||
async def send_test_email(
|
||||
data: TestEmailIn,
|
||||
admin: Annotated[User, Depends(require_admin)],
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
) -> TestEmailOut:
|
||||
"""Отправить тестовое письмо синхронно — проверка почтовой конфигурации без
|
||||
регистрации фиктивного пользователя.
|
||||
|
||||
Получатель по умолчанию — email текущего администратора. Отправка идёт
|
||||
напрямую из эндпоинта (не через Celery), чтобы результат был виден сразу.
|
||||
Секреты SMTP (логин/пароль) в ответе не участвуют — только хост/порт,
|
||||
и то лишь при `EMAIL_BACKEND=smtp` (см. `.env.example`).
|
||||
"""
|
||||
app_settings = get_app_settings()
|
||||
recipient = data.to or admin.email
|
||||
cfg = await InstanceSettingsService(session).get()
|
||||
reply_to = cfg.contact_email if cfg.contact_email_enabled else None
|
||||
is_smtp = app_settings.email_backend == "smtp"
|
||||
smtp_host = app_settings.smtp_host if is_smtp else None
|
||||
smtp_port = app_settings.smtp_port if is_smtp else None
|
||||
|
||||
backend = create_email_backend(app_settings)
|
||||
try:
|
||||
await backend.send(
|
||||
to=recipient,
|
||||
subject="Тестовое письмо VidConf",
|
||||
body="Это тестовое письмо для проверки почтовой конфигурации инстанса VidConf.",
|
||||
reply_to=reply_to,
|
||||
)
|
||||
except EmailSendError as exc:
|
||||
return TestEmailOut(
|
||||
success=False, message=str(exc), smtp_host=smtp_host, smtp_port=smtp_port
|
||||
)
|
||||
return TestEmailOut(
|
||||
success=True,
|
||||
message=f"Письмо успешно отправлено на {recipient}",
|
||||
smtp_host=smtp_host,
|
||||
smtp_port=smtp_port,
|
||||
)
|
||||
|
||||
|
||||
def _to_settings_out(cfg: InstanceConfig, *, transcription_queue_served: bool) -> SettingsOut:
|
||||
"""Собрать `SettingsOut` из эффективной конфигурации + доступность уровней AI."""
|
||||
return SettingsOut(
|
||||
@@ -410,6 +461,8 @@ def _to_settings_out(cfg: InstanceConfig, *, transcription_queue_served: bool) -
|
||||
registration_team_choice=cfg.registration_team_choice,
|
||||
registration_email_domain_enabled=cfg.registration_email_domain_enabled,
|
||||
registration_email_domain=cfg.registration_email_domain,
|
||||
contact_email_enabled=cfg.contact_email_enabled,
|
||||
contact_email=cfg.contact_email,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user