-
Notifications
You must be signed in to change notification settings - Fork 552
feat: migrate email from SendGrid to SMTP with fallback #4566
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
Changes from all commits
6eaecdd
445fc3f
585c6ad
c9d112d
01a6e6d
1706e43
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
+59
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. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find SMTP client constructions and check whether timeout is passed.
rg -nP --type=py -C2 'smtplib\.SMTP\(' apiRepository: Agenta-AI/agenta Length of output: 690 Add SMTP connection timeout to prevent request hangs on stalled mail servers
|
||
| 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() | ||
|
|
||
|
|
||
| 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 | ||
|
Comment on lines
+106
to
108
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. Avoid logging recipient email and subject in disabled-email mode. Line 107 logs direct recipient and subject values, which can expose PII in normal operational logs. Proposed fix- log.info(f"[EMAIL] Email disabled - would send '{subject}' to {to_email}")
+ log.info("[EMAIL] Email disabled - skipped outbound message") |
||
|
|
||
| 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)) | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -922,12 +922,40 @@ def enabled(self) -> bool: | |||||
|
|
||||||
|
|
||||||
| # --------------------------------------------------------------------------- | ||||||
| # sendgrid | ||||||
| # smtp | ||||||
| # --------------------------------------------------------------------------- | ||||||
|
|
||||||
|
|
||||||
| class SmtpConfig(BaseModel): | ||||||
| """SMTP Email configuration""" | ||||||
|
|
||||||
| host: str | None = os.getenv("SMTP_HOST") | ||||||
| port: int = int(os.getenv("SMTP_PORT", "587")) | ||||||
| username: str | None = os.getenv("SMTP_USERNAME") | ||||||
| password: str | None = os.getenv("SMTP_PASSWORD") | ||||||
| from_address: str | None = ( | ||||||
| os.getenv("SMTP_FROM_ADDRESS") | ||||||
| or os.getenv("SENDGRID_FROM_ADDRESS") | ||||||
| or os.getenv("AGENTA_AUTHN_EMAIL_FROM") | ||||||
| or os.getenv("AGENTA_SEND_EMAIL_FROM_ADDRESS") | ||||||
| ) | ||||||
| use_tls: bool = os.getenv("SMTP_USE_TLS", "true").lower() in ("true", "1", "yes") | ||||||
|
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. Use Line 942 currently accepts only Proposed fix- use_tls: bool = os.getenv("SMTP_USE_TLS", "true").lower() in ("true", "1", "yes")
+ use_tls: bool = (os.getenv("SMTP_USE_TLS") or "true").lower() in _TRUTHY📝 Committable suggestion
Suggested change
|
||||||
|
|
||||||
| model_config = ConfigDict(extra="ignore") | ||||||
|
|
||||||
| @property | ||||||
| def enabled(self) -> bool: | ||||||
| """SMTP enabled if host and from address are present""" | ||||||
| return bool(self.host and self.from_address) | ||||||
|
|
||||||
|
|
||||||
| # --------------------------------------------------------------------------- | ||||||
| # sendgrid (legacy — kept for backwards compatibility) | ||||||
| # --------------------------------------------------------------------------- | ||||||
|
|
||||||
|
|
||||||
| class SendgridConfig(BaseModel): | ||||||
| """SendGrid Email configuration""" | ||||||
| """SendGrid Email configuration (legacy)""" | ||||||
|
|
||||||
| api_key: str | None = os.getenv("SENDGRID_API_KEY") | ||||||
| from_address: str | None = ( | ||||||
|
|
@@ -1037,15 +1065,9 @@ def email_method(self) -> str: | |||||
| if env.agenta.access.email_disabled: | ||||||
| return "" | ||||||
|
|
||||||
| sendgrid_enabled = bool( | ||||||
| os.getenv("SENDGRID_API_KEY") | ||||||
| and ( | ||||||
| os.getenv("SENDGRID_FROM_ADDRESS") | ||||||
| or os.getenv("AGENTA_AUTHN_EMAIL_FROM") | ||||||
| or os.getenv("AGENTA_SEND_EMAIL_FROM_ADDRESS") | ||||||
| ) | ||||||
| ) | ||||||
| return "otp" if sendgrid_enabled else "password" | ||||||
| # SMTP takes priority, then SendGrid fallback | ||||||
| email_configured = env.smtp.enabled or env.sendgrid.enabled | ||||||
| return "otp" if email_configured else "password" | ||||||
|
|
||||||
| @property | ||||||
| def email_enabled(self) -> bool: | ||||||
|
|
@@ -1101,6 +1123,7 @@ class EnvironSettings(BaseModel): | |||||
| posthog: PostHogConfig = PostHogConfig() | ||||||
| redis: RedisConfig = RedisConfig() | ||||||
| sendgrid: SendgridConfig = SendgridConfig() | ||||||
| smtp: SmtpConfig = SmtpConfig() | ||||||
| stripe: StripeConfig = StripeConfig() | ||||||
| supertokens: SuperTokensConfig = SuperTokensConfig() | ||||||
|
|
||||||
|
|
||||||
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.
🧩 Analysis chain
🏁 Script executed:
Repository: Agenta-AI/agenta
Length of output: 1458
Guard legacy SendGrid import to prevent startup crash when SendGrid env is enabled but
sendgridisn’t installed.rgfound nosendgriddeclarations in any**/pyproject.tomlor**/requirements*.txt, so the module-scopeimport sendgridcan raiseImportErrorduring startup.Proposed fix
📝 Committable suggestion