ADR-001: Email Provider Abstraction Pattern
Status: Accepted Date: December 17, 2025 Decision Makers: CODITECT Engineering Team
Context
CODITECT requires email capabilities for meeting notifications, reminders, AI-powered email analysis, and workflow automation. While Gmail is the primary provider, we need a design that:
- Allows future support for other email providers (Outlook, SMTP)
- Enables consistent behavior across different providers
- Supports provider-agnostic business logic in integration-core
Decision
We will implement an Email Provider Abstraction Pattern using a Python Protocol/Abstract Base Class that defines the interface all email providers must implement.
Interface Design
class EmailProviderInterface(ABC):
"""Abstract interface for email providers."""
@property
@abstractmethod
def provider_name(self) -> str:
"""Return provider identifier (e.g., 'gmail', 'outlook')."""
pass
@abstractmethod
async def authenticate(self, credentials: Dict[str, Any]) -> bool:
"""Authenticate with the email provider."""
pass
@abstractmethod
async def send_email(
self,
to: List[str],
subject: str,
body: str,
cc: List[str] = None,
bcc: List[str] = None,
attachments: List[Attachment] = None,
html_body: str = None,
) -> EmailMessage:
"""Send an email message."""
pass
@abstractmethod
async def get_message(self, message_id: str) -> Optional[EmailMessage]:
"""Retrieve a specific email message."""
pass
@abstractmethod
async def list_messages(
self,
query: str = None,
max_results: int = 100,
) -> Tuple[List[EmailMessage], Optional[str]]:
"""List email messages with optional filtering."""
pass
@abstractmethod
async def search(self, query: str) -> List[EmailMessage]:
"""Search emails using provider-specific query syntax."""
pass
@abstractmethod
async def watch_inbox(
self,
callback: Callable,
label_ids: List[str] = None,
) -> str:
"""Start watching inbox for new messages."""
pass
@abstractmethod
async def stop_watch(self, watch_id: str) -> bool:
"""Stop watching inbox."""
pass
Implementation Pattern
class GmailProvider(EmailProviderInterface):
@property
def provider_name(self) -> str:
return "gmail"
async def send_email(self, to, subject, body, **kwargs) -> EmailMessage:
# Gmail-specific MIME message building and API call
...
class OutlookProvider(EmailProviderInterface):
@property
def provider_name(self) -> str:
return "outlook"
async def send_email(self, to, subject, body, **kwargs) -> EmailMessage:
# Microsoft Graph API call
...
Consequences
Positive
- Provider Independence: Business logic doesn't depend on Gmail-specific code
- Easy Provider Addition: New providers implement the same interface
- Testability: Can mock providers for unit testing
- Consistent API: Same methods work across all providers
Negative
- Lowest Common Denominator: Some provider-specific features may be hidden
- Abstraction Overhead: Extra layer between business logic and API
Mitigation
- Provider-specific features accessible via
**kwargsor provider subclasses - Thin abstraction layer with minimal overhead
Alternatives Considered
- Direct Gmail API Usage: Simpler but locks us into Gmail
- Generic SMTP: Limited features (no read, no labels, no push)
- Third-party Abstraction (e.g., Nylas): External dependency and cost
Related Decisions
- ADR-002: Zero-Cost Architecture
- ADR-003: OAuth Strategy
- Google Meet ADR-001: Meeting Provider Abstraction (parallel pattern)
Document Control:
- Created: December 17, 2025
- Author: CODITECT Engineering Team