90 lines
2.7 KiB
Python
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)
|