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

View File

@@ -0,0 +1,48 @@
from abc import ABC, abstractmethod
from typing import Iterator, Dict, Any
class MailIntakeAdapter(ABC):
"""
Base adapter interface for mail providers.
This interface defines the minimal contract required for
read-only mail ingestion. No provider-specific concepts
should leak beyond implementations of this class.
"""
@abstractmethod
def iter_message_refs(self, query: str) -> Iterator[Dict[str, str]]:
"""
Iterate over lightweight message references.
Must yield dictionaries containing at least:
- message_id
- thread_id
Example yield:
{
"message_id": "...",
"thread_id": "..."
}
"""
raise NotImplementedError
@abstractmethod
def fetch_message(self, message_id: str) -> Dict[str, Any]:
"""
Fetch a full raw message by message_id.
Returns the provider-native message payload
(e.g., Gmail message JSON).
"""
raise NotImplementedError
@abstractmethod
def fetch_thread(self, thread_id: str) -> Dict[str, Any]:
"""
Fetch a full raw thread by thread_id.
Returns the provider-native thread payload.
"""
raise NotImplementedError

View File

@@ -0,0 +1,105 @@
from typing import Iterator, Dict, Any
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from mail_intake.adapters.base import MailIntakeAdapter
from mail_intake.exceptions import MailIntakeAdapterError
from mail_intake.auth.base import MailIntakeAuthProvider
class MailIntakeGmailAdapter(MailIntakeAdapter):
"""
Gmail read-only adapter.
This class is the ONLY place where:
- googleapiclient is imported
- Gmail REST semantics are known
- .execute() is called
It must remain thin and dumb by design.
"""
def __init__(
self,
auth_provider: MailIntakeAuthProvider,
user_id: str = "me",
):
self._auth_provider = auth_provider
self._user_id = user_id
self._service = None
@property
def service(self):
if self._service is None:
try:
creds = self._auth_provider.get_credentials()
self._service = build("gmail", "v1", credentials=creds)
except Exception as exc:
raise MailIntakeAdapterError(
"Failed to initialize Gmail service"
) from exc
return self._service
def iter_message_refs(self, query: str) -> Iterator[Dict[str, str]]:
"""
Iterate over message references matching the query.
Yields:
{
"message_id": "...",
"thread_id": "..."
}
"""
try:
request = (
self.service.users()
.messages()
.list(userId=self._user_id, q=query)
)
while request is not None:
response = request.execute()
for msg in response.get("messages", []):
yield {
"message_id": msg["id"],
"thread_id": msg["threadId"],
}
request = (
self.service.users()
.messages()
.list_next(request, response)
)
except HttpError as exc:
raise MailIntakeAdapterError(
"Gmail API error while listing messages"
) from exc
def fetch_message(self, message_id: str) -> Dict[str, Any]:
try:
return (
self.service.users()
.messages()
.get(userId=self._user_id, id=message_id)
.execute()
)
except HttpError as exc:
raise MailIntakeAdapterError(
f"Gmail API error while fetching message {message_id}"
) from exc
def fetch_thread(self, thread_id: str) -> Dict[str, Any]:
try:
return (
self.service.users()
.threads()
.get(userId=self._user_id, id=thread_id)
.execute()
)
except HttpError as exc:
raise MailIntakeAdapterError(
f"Gmail API error while fetching thread {thread_id}"
) from exc