feat: replace SendGrid with Gmail API via Domain-Wide Delegation
Some checks are pending
Check Python Version Consistency / Check Python Version (push) Waiting to run

This commit is contained in:
Chris Christiansen 2026-07-02 00:12:43 +00:00
parent e744a1d510
commit bc0ee3b3ee

50
main.py
View File

@ -57,8 +57,7 @@ from functools import wraps
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
from authlib.integrations.starlette_client import OAuth from authlib.integrations.starlette_client import OAuth
import sendgrid
from sendgrid.helpers.mail import Mail
AGENT_ID = "osvauco-opax" AGENT_ID = "osvauco-opax"
PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5") PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
@ -305,7 +304,7 @@ async def vm_ssh_key(instance: str = "osvauco-dev-vm"):
@app.get("/notify/channels") @app.get("/notify/channels")
async def get_notify_channels(): async def get_notify_channels():
return {"channels": [ return {"channels": [
{"type": "email", "configured": bool(os.getenv("SENDGRID_API_KEY")), "target": os.getenv("NOTIFY_EMAIL_TO")}, {"type": "email", "configured": bool(os.getenv("GMAIL_DEFAULT_SENDER")), "target": os.getenv("NOTIFY_EMAIL_TO")},
{"type": "sms", "configured": bool(os.getenv("TWILIO_ACCOUNT_SID")), "target": os.getenv("TWILIO_FROM_NUMBER")}, {"type": "sms", "configured": bool(os.getenv("TWILIO_ACCOUNT_SID")), "target": os.getenv("TWILIO_FROM_NUMBER")},
{"type": "webhook", "configured": bool(os.getenv("NOTIFY_WEBHOOK_URL")), "url": os.getenv("NOTIFY_WEBHOOK_URL")}, {"type": "webhook", "configured": bool(os.getenv("NOTIFY_WEBHOOK_URL")), "url": os.getenv("NOTIFY_WEBHOOK_URL")},
]} ]}
@ -473,20 +472,49 @@ class NotificationService:
return {"status": "error", "detail": str(e)} return {"status": "error", "detail": str(e)}
async def send_email(self, req) -> dict: async def send_email(self, req) -> dict:
api_key = os.environ.get("SENDGRID_API_KEY") import base64
import os
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from google.auth import default as google_auth_default
from googleapiclient.discovery import build
to_email = req.to or ALERT_EMAIL to_email = req.to or ALERT_EMAIL
if not api_key:
return {"status": "not_configured", "detail": "SENDGRID_API_KEY mangler"}
if not to_email: if not to_email:
return {"status": "not_configured", "detail": "Ingen mottaker-e-post"} return {"status": "skipped", "reason": "no recipient"}
allowed_senders_str = os.getenv("GMAIL_ALLOWED_SENDERS", "")
allowed = [s.strip() for s in allowed_senders_str.split(",") if s.strip()]
sender = os.getenv("GMAIL_DEFAULT_SENDER")
if not sender or not allowed:
return {"status": "not_configured", "reason": "GMAIL_DEFAULT_SENDER or GMAIL_ALLOWED_SENDERS not set"}
if sender not in allowed:
return {"status": "error", "reason": f"Default sender {sender} is not in the allowed list"}
meta = self._event_meta(req.event) meta = self._event_meta(req.event)
subject = req.subject or f"{meta['emoji']} CostGuard: {meta['label']}{datetime.date.today()}" subject = req.subject or f"{meta['emoji']} CostGuard: {meta['label']}{datetime.date.today()}"
html_content = req.body_html if req.body_html else await self._build_digest_html(req.payload) html_content = req.body_html if req.body_html else await self._build_digest_html(req.payload)
try: try:
message = Mail(from_email="costguard@osvauco.no", to_emails=to_email, subject=subject, html_content=html_content) credentials, _ = google_auth_default(scopes=["https://www.googleapis.com/auth/gmail.send"])
sg = sendgrid.SendGridAPIClient(api_key) credentials = credentials.with_subject(sender)
response = sg.send(message)
return {"status": "ok", "message_id": response.headers.get("X-Message-Id", ""), "to": to_email, "event": req.event} service = build("gmail", "v1", credentials=credentials)
message = MIMEMultipart("alternative")
message["to"] = to_email
message["from"] = sender
message["subject"] = subject
message.attach(MIMEText(html_content, "html"))
raw = base64.urlsafe_b64encode(message.as_bytes()).decode()
result = service.users().messages().send(
userId="me", body={"raw": raw}
).execute()
return {"status": "ok", "message_id": result.get("id", ""), "to": to_email, "event": req.event}
except Exception as e: except Exception as e:
return {"status": "error", "detail": str(e)} return {"status": "error", "detail": str(e)}