File size: 2,428 Bytes
5ff7dfd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | """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}")
|