All checks were successful
CI / test (push) Successful in 21s
Email delivery: - Replaced logging-only email sender with aiosmtplib SMTP implementation. - Added provider mode switch via EMAIL_PROVIDER (log/smtp). - Added TLS/SSL and timeout controls for SMTP transport. Auth registration flow: - Made register/resend/reset email flows transactional with rollback on delivery failure. - Return 503 when verification/reset email cannot be delivered. Configuration: - Extended settings and env templates for EMAIL_PROVIDER, SMTP_USE_SSL, SMTP_TIMEOUT_SECONDS. - Updated docker-compose environment mapping for new SMTP variables.
76 lines
2.8 KiB
Python
76 lines
2.8 KiB
Python
import logging
|
|
from email.message import EmailMessage
|
|
|
|
import aiosmtplib
|
|
|
|
from app.config.settings import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class EmailDeliveryError(Exception):
|
|
pass
|
|
|
|
|
|
class EmailService:
|
|
async def send_verification_email(self, email: str, token: str) -> None:
|
|
verify_link = f"{settings.frontend_base_url}/verify-email?token={token}"
|
|
subject = "Verify your BenyaMessenger account"
|
|
text = (
|
|
"Welcome to BenyaMessenger.\n\n"
|
|
f"Verify your email by opening this link:\n{verify_link}\n\n"
|
|
"If you did not create this account, ignore this email."
|
|
)
|
|
html = (
|
|
"<p>Welcome to <b>BenyaMessenger</b>.</p>"
|
|
f"<p>Verify your email by opening this link:<br><a href='{verify_link}'>{verify_link}</a></p>"
|
|
"<p>If you did not create this account, ignore this email.</p>"
|
|
)
|
|
await self._send_email(email, subject, text, html)
|
|
|
|
async def send_password_reset_email(self, email: str, token: str) -> None:
|
|
reset_link = f"{settings.frontend_base_url}/reset-password?token={token}"
|
|
subject = "Reset your BenyaMessenger password"
|
|
text = (
|
|
"Password reset request.\n\n"
|
|
f"Open this link to reset your password:\n{reset_link}\n\n"
|
|
"If you did not request this, ignore this email."
|
|
)
|
|
html = (
|
|
"<p>Password reset request.</p>"
|
|
f"<p>Open this link to reset your password:<br><a href='{reset_link}'>{reset_link}</a></p>"
|
|
"<p>If you did not request this, ignore this email.</p>"
|
|
)
|
|
await self._send_email(email, subject, text, html)
|
|
|
|
async def _send_email(self, recipient: str, subject: str, text: str, html: str) -> None:
|
|
if settings.email_provider.lower() != "smtp":
|
|
logger.info("EMAIL to=%s subject=%s body=%s", recipient, subject, text)
|
|
return
|
|
|
|
message = EmailMessage()
|
|
message["From"] = settings.smtp_from_email
|
|
message["To"] = recipient
|
|
message["Subject"] = subject
|
|
message.set_content(text)
|
|
message.add_alternative(html, subtype="html")
|
|
|
|
try:
|
|
await aiosmtplib.send(
|
|
message,
|
|
hostname=settings.smtp_host,
|
|
port=settings.smtp_port,
|
|
username=settings.smtp_username or None,
|
|
password=settings.smtp_password or None,
|
|
start_tls=settings.smtp_use_tls,
|
|
use_tls=settings.smtp_use_ssl,
|
|
timeout=settings.smtp_timeout_seconds,
|
|
)
|
|
except Exception as exc:
|
|
logger.exception("SMTP delivery failed to=%s subject=%s", recipient, subject)
|
|
raise EmailDeliveryError("Email delivery failed") from exc
|
|
|
|
|
|
def get_email_service() -> EmailService:
|
|
return EmailService()
|