backend/mailer.py (2206 bytes)
1 """Outbound email. Plain SMTP, so any provider works; nothing else is sent.""" 2 3 from __future__ import annotations 4 5 import logging 6 import smtplib 7 from email.message import EmailMessage 8 9 from .settings import settings 10 11 log = logging.getLogger(__name__) 12 13 14 class MailError(RuntimeError): 15 pass 16 17 18 def send_login_link(to: str, url: str) -> bool: 19 """Email a sign-in link. Returns False when there is no mail server. 20 21 That only happens in development (dev_login_links), where the API hands 22 the link back itself. It is deliberately not logged: a sign-in link in a 23 log file is a credential in a log file. 24 """ 25 if not settings.email_configured: 26 return False 27 28 minutes = settings.login_link_minutes 29 msg = EmailMessage() 30 msg["Subject"] = "Your sign-in link" 31 msg["From"] = settings.smtp_from 32 msg["To"] = to 33 msg.set_content( 34 f"Open this link to sign in:\n\n{url}\n\n" 35 f"It works once and expires in {minutes} minutes. " 36 "If you did not ask for it, ignore this email - nothing happens " 37 "unless the link is opened.\n" 38 ) 39 msg.add_alternative( 40 f"<p>Open this link to sign in:</p>" 41 f'<p><a href="{url}">Sign in</a></p>' 42 f'<p style="color:#666;font-size:13px">It works once and expires in ' 43 f"{minutes} minutes. If you did not ask for it, ignore this email - " 44 f"nothing happens unless the link is opened.</p>", 45 subtype="html", 46 ) 47 48 try: 49 if settings.smtp_port == 465: 50 smtp = smtplib.SMTP_SSL(settings.smtp_host, settings.smtp_port, timeout=20) 51 else: 52 smtp = smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=20) 53 with smtp: 54 if settings.smtp_port != 465 and settings.smtp_starttls: 55 smtp.starttls() 56 if settings.smtp_user: 57 smtp.login(settings.smtp_user, settings.smtp_password) 58 smtp.send_message(msg) 59 except (smtplib.SMTPException, OSError) as exc: 60 log.warning("could not send sign-in link to %s: %s", to, exc) 61 raise MailError( 62 "The sign-in email could not be sent. Try again shortly." 63 ) from exc 64 return True