lib init
This commit is contained in:
0
mail_intake/auth/__init__.py
Normal file
0
mail_intake/auth/__init__.py
Normal file
20
mail_intake/auth/base.py
Normal file
20
mail_intake/auth/base.py
Normal 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
|
||||
81
mail_intake/auth/google.py
Normal file
81
mail_intake/auth/google.py
Normal 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
|
||||
Reference in New Issue
Block a user