| """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}") |
|
|