infrasynth-backend-kit/infrasynth/notifications/channels/email_sendgrid.py
2026-08-28 14:38:47 -05:00

90 lines
2.7 KiB
Python

import base64
import logging
import requests
from infrasynth.shared.enums import ChannelType
from infrasynth.shared.results import Result
from infrasynth.shared.settings_utils import get_setting
from .base import BaseChannel
logger = logging.getLogger(__name__)
class SendGridChannel(BaseChannel):
"""Email channel that sends via the SendGrid v3 Mail Send API.
Configuration keys (read from ``ChannelConfig.config``):
- ``api_key`` (required)
- ``from_email`` (falls back to DEFAULT_FROM_EMAIL)
- ``timeout``
"""
channel_type = ChannelType.EMAIL
API_URL = "https://api.sendgrid.com/v3/mail/send"
def __init__(self, api_key=None, from_email=None, timeout=30):
self.api_key = api_key
self.from_email = from_email or get_setting(
"INFRASYNTH_NOTIFICATIONS", "DEFAULT_FROM_EMAIL", "noreply@example.com"
)
self.timeout = timeout
@classmethod
def from_config(cls, config: dict) -> "SendGridChannel":
config = {str(key).lower(): value for key, value in (config or {}).items()}
return cls(**config)
def send(
self,
recipient: str,
subject: str,
body: str,
is_html: bool = True,
attachments=None,
) -> Result[bool, str]:
if not self.api_key:
return Result.err("SendGrid API key not configured")
payload = {
"personalizations": [{"to": [{"email": recipient}]}],
"from": {"email": self.from_email},
"subject": subject,
"content": [
{
"type": "text/html" if is_html else "text/plain",
"value": body,
}
],
}
if attachments:
payload["attachments"] = [
{
"filename": attachment.filename,
"content": base64.b64encode(attachment.content).decode(),
"type": attachment.mime_type,
}
for attachment in attachments
]
try:
response = requests.post(
self.API_URL,
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
},
json=payload,
timeout=self.timeout,
)
except requests.RequestException as exc:
logger.exception("SendGrid request failed for %s", recipient)
return Result.err(str(exc))
if response.status_code >= 400:
return Result.err(f"SendGrid error {response.status_code}: {response.text[:500]}")
return Result.ok(True)
def health_check(self) -> bool:
return bool(self.api_key)