"""Transactional account-security email (verification codes, password reset codes, lockout notices) - deliberately separate from the alert- notification path in app/services/alert_service.py, which dispatches to user-configured destinations via app/notifications/factory.py. This is always sent to the account's own registered email, from a distinct sender identity (settings.resend_security_from_email, e.g. security@ciagent.org vs. alerts@ciagent.org). Picks the Resend HTTP API provider when RESEND_API_KEY is configured, else falls back to the existing SmtpEmailProvider. `resolve_provider` is also reused by app.services.unban_service for the admin unban-request notification, which has the same "Resend if configured, else SMTP" needs. """ from __future__ import annotations from app.core.config import Settings from app.notifications.base import DeliveryResult, NotificationMessage from app.notifications.resend_email import ResendEmailProvider from app.notifications.smtp_email import SmtpEmailProvider def resolve_provider(settings: Settings): if settings.resend_api_key: return ResendEmailProvider(settings) return SmtpEmailProvider(settings) async def send_verification_code_email(settings: Settings, to: str, code: str) -> DeliveryResult: message = NotificationMessage( destination_value=to, subject="Verify your CI Agent account", body_text=( f"Your verification code is {code}.\n\n" "This code expires in 36 hours. If you didn't request this, you can ignore this email." ), body_html=( f"
Your verification code is {code}.
" "This code expires in 36 hours. If you didn't request this, you can ignore this email.
" ), ) return await resolve_provider(settings).send(message) async def send_password_reset_email(settings: Settings, to: str, code: str) -> DeliveryResult: message = NotificationMessage( destination_value=to, subject="Reset your CI Agent password", body_text=( f"Your password reset code is {code}.\n\n" "This code expires in 36 hours. If you didn't request this, you can ignore this email " "and your password will stay unchanged." ), body_html=( f"Your password reset code is {code}.
" "This code expires in 36 hours. If you didn't request this, you can ignore this " "email and your password will stay unchanged.
" ), ) return await resolve_provider(settings).send(message) async def send_account_locked_email(settings: Settings, to: str) -> DeliveryResult: reset_link = f"{settings.frontend_url}/forgot-password" message = NotificationMessage( destination_value=to, subject="Your CI Agent account was locked", body_text=( "Your account was locked after repeated failed login attempts.\n\n" f"To unlock it, reset your password: {reset_link}" ), body_html=( "Your account was locked after repeated failed login attempts.
" f'To unlock it, reset your password.
' ), ) return await resolve_provider(settings).send(message)