-
Notifications
You must be signed in to change notification settings - Fork 540
feat: custom passwordless email delivery via shared email service #4570
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6eaecdd
445fc3f
585c6ad
c9d112d
01a6e6d
1706e43
ad6e140
cd01041
77f3ec1
cdc05d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| import os | ||
|
|
||
| import sendgrid | ||
| from sendgrid.helpers.mail import Mail | ||
| import smtplib | ||
| from email.mime.multipart import MIMEMultipart | ||
| from email.mime.text import MIMEText | ||
|
|
||
| from fastapi import HTTPException | ||
|
|
||
|
|
@@ -10,16 +10,25 @@ | |
|
|
||
| log = get_logger(__name__) | ||
|
|
||
| # Initialize SendGrid only if enabled | ||
| if env.sendgrid.enabled: | ||
| sg = sendgrid.SendGridAPIClient(api_key=env.sendgrid.api_key) | ||
| log.info("✓ SendGrid enabled") | ||
| # Determine which email backend to use (SMTP > SendGrid > no-op) | ||
| _USE_SMTP = env.smtp.enabled | ||
| _USE_SENDGRID = not _USE_SMTP and env.sendgrid.enabled | ||
|
|
||
| if _USE_SMTP: | ||
| log.info( | ||
| "✓ Email enabled via SMTP (%s:%s)", env.smtp.host, env.smtp.port | ||
| ) | ||
| elif _USE_SENDGRID: | ||
| import sendgrid | ||
|
|
||
| _sg = sendgrid.SendGridAPIClient(api_key=env.sendgrid.api_key) | ||
| log.info("✓ Email enabled via SendGrid (legacy)") | ||
| else: | ||
| sg = None | ||
| _sg = None | ||
| if env.sendgrid.api_key and not env.sendgrid.from_address: | ||
| log.warn("✗ SendGrid disabled: missing sender email address") | ||
| log.warn("✗ Email disabled: missing sender email address") | ||
| else: | ||
| log.warn("✗ SendGrid disabled") | ||
| log.warn("✗ Email disabled") | ||
|
|
||
|
|
||
| def read_email_template(template_file_path): | ||
|
|
@@ -35,6 +44,46 @@ def read_email_template(template_file_path): | |
| return template_file.read() | ||
|
|
||
|
|
||
| def _send_via_smtp(to_email: str, subject: str, html_content: str, from_email: str) -> None: | ||
| """Send email using SMTP.""" | ||
| msg = MIMEMultipart("alternative") | ||
| msg["Subject"] = subject | ||
| msg["From"] = from_email | ||
| msg["To"] = to_email | ||
| msg.attach(MIMEText(html_content, "html")) | ||
|
|
||
| smtp_host = env.smtp.host | ||
| smtp_port = env.smtp.port | ||
|
|
||
| if env.smtp.use_tls: | ||
| server = smtplib.SMTP(smtp_host, smtp_port) | ||
| server.ehlo() | ||
| server.starttls() | ||
| server.ehlo() | ||
| else: | ||
| server = smtplib.SMTP(smtp_host, smtp_port) | ||
|
|
||
|
Comment on lines
+58
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add implicit SSL SMTP mode support. Line 58-Line 65 only switches between STARTTLS and plain |
||
| try: | ||
| if env.smtp.username and env.smtp.password: | ||
| server.login(env.smtp.username, env.smtp.password) | ||
| server.sendmail(from_email, [to_email], msg.as_string()) | ||
| finally: | ||
| server.quit() | ||
|
Comment on lines
+59
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move SMTP connection setup inside a context-managed At Line 59-Line 63, connection/handshake exceptions can occur before the |
||
|
|
||
|
|
||
| def _send_via_sendgrid(to_email: str, subject: str, html_content: str, from_email: str) -> None: | ||
| """Send email using SendGrid (legacy fallback).""" | ||
| from sendgrid.helpers.mail import Mail | ||
|
|
||
| message = Mail( | ||
| from_email=from_email, | ||
| to_emails=to_email, | ||
| subject=subject, | ||
| html_content=html_content, | ||
| ) | ||
| _sg.send(message) | ||
|
|
||
|
|
||
| async def send_email( | ||
| to_email: str, subject: str, html_content: str, from_email: str | ||
| ) -> bool: | ||
|
|
@@ -54,20 +103,15 @@ async def send_email( | |
| HTTPException: If there is an error sending the email. | ||
| """ | ||
|
|
||
| # No-op if SendGrid is disabled | ||
| if not env.sendgrid.enabled: | ||
| log.info(f"[SENDGRID] Email disabled - would send '{subject}' to {to_email}") | ||
| if not _USE_SMTP and not _USE_SENDGRID: | ||
| log.info(f"[EMAIL] Email disabled - would send '{subject}' to {to_email}") | ||
| return True | ||
|
|
||
| message = Mail( | ||
| from_email=from_email, | ||
| to_emails=to_email, | ||
| subject=subject, | ||
| html_content=html_content, | ||
| ) | ||
|
|
||
| try: | ||
| sg.send(message) | ||
| if _USE_SMTP: | ||
| _send_via_smtp(to_email, subject, html_content, from_email) | ||
| else: | ||
| _send_via_sendgrid(to_email, subject, html_content, from_email) | ||
| return True | ||
| except Exception as e: | ||
| raise HTTPException(status_code=500, detail=str(e)) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not swallow OTP delivery failures.
At Line 119-Line 124, all exceptions are logged and then suppressed. That can make passwordless login appear successful while the OTP email was never delivered. Re-raise after logging so callers can surface a failure state.