This commit is contained in:
2026-01-03 05:21:55 +05:30
parent 278f0a3d40
commit 412a9c7bec
22 changed files with 950 additions and 0 deletions

View File

20
mail_intake/auth/base.py Normal file
View File

@@ -0,0 +1,20 @@
from abc import ABC, abstractmethod
class MailIntakeAuthProvider(ABC):
"""
Abstract authentication provider.
Mail adapters depend on this interface, not on concrete
OAuth or credential implementations.
"""
@abstractmethod
def get_credentials(self):
"""
Return provider-specific credentials object.
This method is synchronous by design and must either
return valid credentials or raise MailIntakeAuthError.
"""
raise NotImplementedError

View File

@@ -0,0 +1,81 @@
import os
import pickle
from typing import Sequence
import google.auth.exceptions
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow
from mail_intake.auth.base import MailIntakeAuthProvider
from mail_intake.exceptions import MailIntakeAuthError
class MailIntakeGoogleAuth(MailIntakeAuthProvider):
"""
Google OAuth provider for Gmail access.
Responsibilities:
- Load cached credentials from disk
- Refresh expired tokens when possible
- Trigger interactive login only when strictly required
This class is synchronous and intentionally state-light.
"""
def __init__(
self,
credentials_path: str,
token_path: str,
scopes: Sequence[str],
):
self.credentials_path = credentials_path
self.token_path = token_path
self.scopes = list(scopes)
def get_credentials(self):
creds = None
# Attempt to load cached credentials
if os.path.exists(self.token_path):
try:
with open(self.token_path, "rb") as fh:
creds = pickle.load(fh)
except Exception:
creds = None
# Validate / refresh credentials
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
try:
creds.refresh(Request())
except google.auth.exceptions.RefreshError:
creds = None
# Interactive login if refresh failed or creds missing
if not creds:
if not os.path.exists(self.credentials_path):
raise MailIntakeAuthError(
f"Google credentials file not found: {self.credentials_path}"
)
try:
flow = InstalledAppFlow.from_client_secrets_file(
self.credentials_path,
self.scopes,
)
creds = flow.run_local_server(port=0)
except Exception as exc:
raise MailIntakeAuthError(
"Failed to complete Google OAuth flow"
) from exc
# Persist refreshed / new credentials
try:
with open(self.token_path, "wb") as fh:
pickle.dump(creds, fh)
except Exception as exc:
raise MailIntakeAuthError(
f"Failed to write token file: {self.token_path}"
) from exc
return creds