"""Send messages via Gmail API using a stored OAuth refresh token (gmail.send scope).""" from __future__ import annotations import base64 import os from email.message import EmailMessage import httpx GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token" GMAIL_SEND_URL = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send" async def refresh_access_token(refresh_token: str) -> str: client_id = os.environ.get("GOOGLE_CLIENT_ID", "").strip() client_secret = os.environ.get("GOOGLE_CLIENT_SECRET", "").strip() if not client_id or not client_secret: raise ValueError("Google OAuth is not configured") async with httpx.AsyncClient() as client: r = await client.post( GOOGLE_TOKEN_URI, data={ "client_id": client_id, "client_secret": client_secret, "refresh_token": refresh_token, "grant_type": "refresh_token", }, headers={"Content-Type": "application/x-www-form-urlencoded"}, timeout=30.0, ) if r.status_code != 200: raise ValueError(f"Token refresh failed ({r.status_code}): {r.text[:500]}") data = r.json() at = data.get("access_token") if not at: raise ValueError("No access_token in refresh response") return str(at) def _rfc822_raw(sender: str, to: str, subject: str, body: str) -> str: msg = EmailMessage() msg["Subject"] = subject msg["From"] = sender msg["To"] = to msg.set_content(body) return base64.urlsafe_b64encode(msg.as_bytes()).decode() async def send_invite_email_via_gmail( refresh_token: str, from_email: str, to_email: str, subject: str, body: str, ) -> None: """Raises ValueError on configuration or API errors.""" if not from_email or "@" not in from_email: raise ValueError("Inviter has no email address for Gmail send") access = await refresh_access_token(refresh_token) raw = _rfc822_raw(from_email, to_email, subject, body) async with httpx.AsyncClient() as client: r = await client.post( GMAIL_SEND_URL, headers={"Authorization": f"Bearer {access}"}, json={"raw": raw}, timeout=30.0, ) if r.status_code not in (200, 201): err = r.text[:1500] if r.text else "" raise ValueError(f"Gmail send failed ({r.status_code}): {err}")