Public Access
feat: wire SendGridEmailBackend with from_email/reply_to settings
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@ class Settings(BaseSettings):
|
|||||||
DATABASE_URL: str = ""
|
DATABASE_URL: str = ""
|
||||||
|
|
||||||
SENDGRID_API_KEY: Optional[str] = None
|
SENDGRID_API_KEY: Optional[str] = None
|
||||||
|
SENDGRID_FROM_EMAIL: str = "peter@research.bike"
|
||||||
|
SENDGRID_REPLY_TO: str = "peter@research.bike"
|
||||||
EMAIL_BACKEND: str = "console"
|
EMAIL_BACKEND: str = "console"
|
||||||
LUCKY_CA_URL: str = "https://www.luckyncal.com"
|
LUCKY_CA_URL: str = "https://www.luckyncal.com"
|
||||||
# Swiftly product API (replaces Playwright path). The bearer JWT is
|
# Swiftly product API (replaces Playwright path). The bearer JWT is
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ intentionally left to be wired in R3-C.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Protocol
|
from typing import Optional, Protocol
|
||||||
|
|
||||||
|
from sendgrid import SendGridAPIClient
|
||||||
|
from sendgrid.helpers.mail import Mail, ReplyTo
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
|
|
||||||
@@ -66,11 +68,10 @@ class ConsoleEmailBackend:
|
|||||||
|
|
||||||
|
|
||||||
class SendGridEmailBackend:
|
class SendGridEmailBackend:
|
||||||
"""Stub. Wire in R3-C (real SendGrid client + sandbox mode + retries).
|
def __init__(self) -> None:
|
||||||
|
self._client = SendGridAPIClient(settings.SENDGRID_API_KEY)
|
||||||
Deliberately raises so a misconfigured prod env fails loudly instead of
|
self._from_email = settings.SENDGRID_FROM_EMAIL
|
||||||
silently dropping mail.
|
self._reply_to = settings.SENDGRID_REPLY_TO
|
||||||
"""
|
|
||||||
|
|
||||||
def send(
|
def send(
|
||||||
self,
|
self,
|
||||||
@@ -78,8 +79,20 @@ class SendGridEmailBackend:
|
|||||||
subject: str,
|
subject: str,
|
||||||
html: str,
|
html: str,
|
||||||
text: Optional[str] = None,
|
text: Optional[str] = None,
|
||||||
) -> None: # pragma: no cover - stub
|
) -> None:
|
||||||
raise NotImplementedError("Wire SendGrid in R3-C")
|
message = Mail(
|
||||||
|
from_email=self._from_email,
|
||||||
|
to_emails=to,
|
||||||
|
subject=subject,
|
||||||
|
html_content=html,
|
||||||
|
plain_text_content=text,
|
||||||
|
)
|
||||||
|
message.reply_to = ReplyTo(self._reply_to)
|
||||||
|
response = self._client.send(message)
|
||||||
|
if response.status_code >= 400:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"SendGrid error {response.status_code}: {response.body}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_email_backend() -> EmailBackend:
|
def get_email_backend() -> EmailBackend:
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Unit tests for SendGridEmailBackend — no Postgres needed."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_sendgrid_send_calls_api(monkeypatch):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class FakeResponse:
|
||||||
|
status_code = 202
|
||||||
|
body = b""
|
||||||
|
|
||||||
|
class FakeClient:
|
||||||
|
def send(self, msg):
|
||||||
|
calls.append(msg)
|
||||||
|
return FakeResponse()
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.services.email.SendGridAPIClient", lambda _key: FakeClient())
|
||||||
|
|
||||||
|
from app.services.email import SendGridEmailBackend
|
||||||
|
backend = SendGridEmailBackend()
|
||||||
|
backend.send(to="to@example.com", subject="Hello", html="<p>Hi</p>")
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_sendgrid_raises_on_4xx(monkeypatch):
|
||||||
|
class FakeResponse:
|
||||||
|
status_code = 400
|
||||||
|
body = b"bad request"
|
||||||
|
|
||||||
|
class FakeClient:
|
||||||
|
def send(self, msg):
|
||||||
|
return FakeResponse()
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.services.email.SendGridAPIClient", lambda _key: FakeClient())
|
||||||
|
|
||||||
|
from app.services.email import SendGridEmailBackend
|
||||||
|
backend = SendGridEmailBackend()
|
||||||
|
with pytest.raises(RuntimeError, match="400"):
|
||||||
|
backend.send(to="to@example.com", subject="Hello", html="<p>Hi</p>")
|
||||||
Reference in New Issue
Block a user