Finance-Accounts/ar-aging-app/backend/app/services/mailer.py

67 lines
2.4 KiB
Python

"""Outbound email — used ONLY for password codes. Stdlib smtplib, no new dependencies.
Configured via AR_SMTP_* (see config.py). When unconfigured, callers get MailerError and
the UI falls back to the current-password / admin-reset flows."""
from __future__ import annotations
import logging
import smtplib
import ssl
from email.message import EmailMessage
from ..config import (SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, SMTP_PORT, SMTP_STARTTLS,
SMTP_USER, email_enabled)
logger = logging.getLogger(__name__)
class MailerError(RuntimeError):
"""Email could not be sent (unconfigured, auth failure, network...)."""
def _ssl_context() -> ssl.SSLContext:
"""certifi CA bundle when available — the Windows OS cert store is unreliable on some
machines (same workaround as the FX service)."""
try:
import certifi
return ssl.create_default_context(cafile=certifi.where())
except ImportError:
return ssl.create_default_context()
def send_email(to: str, subject: str, body: str) -> None:
if not email_enabled():
raise MailerError("Email is not configured on this server (AR_SMTP_* settings).")
msg = EmailMessage()
msg["From"] = SMTP_FROM
msg["To"] = to
msg["Subject"] = subject
msg.set_content(body)
try:
if SMTP_STARTTLS:
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=20) as s:
s.starttls(context=_ssl_context())
if SMTP_USER:
s.login(SMTP_USER, SMTP_PASSWORD)
s.send_message(msg)
else: # implicit TLS (port 465)
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=20,
context=_ssl_context()) as s:
if SMTP_USER:
s.login(SMTP_USER, SMTP_PASSWORD)
s.send_message(msg)
logger.info("email sent to %s: %s", to, subject)
except (smtplib.SMTPException, OSError) as e:
raise MailerError(f"Could not send the email: {e}") from e
def send_password_code(to: str, code: str, minutes: int) -> None:
send_email(
to,
"Your password code — Amazon A/R Aging",
f"Your verification code is:\n\n {code}\n\n"
f"It expires in {minutes} minutes and works once.\n\n"
f"If you didn't request a password change, ignore this email — "
f"your password has not been changed.",
)