from __future__ import annotations import time from dataclasses import dataclass import aiohttp @dataclass class OAuth2Token: access_token: str refresh_token: str expires_at: float token_type: str = "Bearer" @property def is_expired(self) -> bool: return time.time() > self.expires_at - 60 class EmailOAuth2Manager: MICROSOFT_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token" GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token" def __init__(self, provider: str, client_id: str, client_secret: str): self._provider = provider self._client_id = client_id self._client_secret = client_secret self._token: OAuth2Token | None = None def _token_url(self) -> str: if self._provider in ("outlook", "office365", "hotmail", "live"): return self.MICROSOFT_TOKEN_URL return self.GOOGLE_TOKEN_URL async def get_access_token(self, refresh_token: str) -> str: if self._token and not self._token.is_expired: return self._token.access_token data = { "client_id": self._client_id, "client_secret": self._client_secret, "refresh_token": refresh_token, "grant_type": "refresh_token", } async with aiohttp.ClientSession() as session: async with session.post( self._token_url(), data=data, timeout=aiohttp.ClientTimeout(total=15) ) as resp: if resp.status != 200: raise RuntimeError(f"OAuth2 token refresh failed: {await resp.text()}") token_data = await resp.json() self._token = OAuth2Token( access_token=token_data["access_token"], refresh_token=token_data.get("refresh_token", refresh_token), expires_at=time.time() + token_data.get("expires_in", 3600), ) return self._token.access_token def build_xoauth2_string(self, username: str, access_token: str) -> str: return f"user={username}\x01auth=Bearer {access_token}\x01\x01"