Files
nexgen_mirrors 41bbdb4bef feat(email): add provider abstraction and HTML templates
Add pluggable email provider system supporting SMTP, Mailgun, and SendGrid
with factory pattern for runtime provider selection. Includes branded HTML
email templates for verification, password reset, MFA notifications, and
organization invites.

Also rebrands all email content from Gatehouse to Secuird, adds email
provider configuration options, and fixes duplicate log handlers in
development mode.
2026-04-04 16:55:00 +10:30

95 lines
3.0 KiB
Python

"""SendGrid email provider implementation."""
import logging
import requests
from flask import current_app
from gatehouse_app.services.email_provider import EmailMessage, EmailProvider
logger = logging.getLogger(__name__)
class SendGridEmailProvider(EmailProvider):
"""SendGrid API-based email provider implementation."""
# Configuration keys
SENDGRID_API_KEY = "SENDGRID_API_KEY"
SENDGRID_FROM_EMAIL = "SENDGRID_FROM_EMAIL"
FROM_ADDRESS = "FROM_ADDRESS"
API_URL = "https://api.sendgrid.com/v3/mail/send"
def send(self, message: EmailMessage) -> bool:
"""Send an email via SendGrid API.
Args:
message: EmailMessage instance containing email details
Returns:
bool: True if email was sent successfully, False otherwise
"""
api_key = current_app.config.get(self.SENDGRID_API_KEY)
default_from = current_app.config.get(self.SENDGRID_FROM_EMAIL)
fallback_from = current_app.config.get(self.FROM_ADDRESS)
if not api_key:
logger.error(
f"[SENDGRID] Cannot send — missing SENDGRID_API_KEY config. "
f"Would have sent to: {message.to} | Subject: {message.subject}"
)
return False
from_address = message.from_address or default_from or fallback_from
if not from_address:
logger.error(
f"[SENDGRID] Cannot send — missing from address (SENDGRID_FROM_EMAIL or FROM_ADDRESS). "
f"Would have sent to: {message.to} | Subject: {message.subject}"
)
return False
payload = {
"personalizations": [
{
"to": [{"email": message.to}]
}
],
"from": {"email": from_address},
"subject": message.subject,
"content": [
{
"type": "text/plain",
"value": message.body
}
]
}
if message.html_body:
payload["content"].append({
"type": "text/html",
"value": message.html_body
})
try:
response = requests.post(
self.API_URL,
json=payload,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
)
if response.status_code == 202:
logger.info(f"[SENDGRID] Sent to {message.to} | Subject: {message.subject}")
return True
else:
logger.error(
f"[SENDGRID] Failed to send to {message.to}: "
f"status={response.status_code} body={response.text}"
)
return False
except Exception as e:
logger.error(f"[SENDGRID] Exception while sending to {message.to}: {e}")
return False