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