nexus-5/core/services/email_service.py
2026-01-26 11:09:40 -05:00

304 lines
9.1 KiB
Python

"""
Emailer Microservice Client
This module provides integration with the Emailer microservice,
a Rust-based REST API for sending emails via Gmail API.
Production URL: https://email.example.com
"""
import requests
from typing import List, Dict, Optional
from django.conf import settings
import logging
logger = logging.getLogger(__name__)
class EmailerServiceError(Exception):
"""Base exception for emailer service errors"""
pass
class EmailerClient:
"""
Client for the Emailer microservice.
Features:
- Template-based emails with variable substitution
- Plain text and HTML email support
- Attachment support
- User impersonation for domain-wide delegation
- Health checking
Example:
emailer = EmailerClient()
emailer.send_template_email(
to=['user@example.com'],
template_id='notification',
variables={
'subject': 'Project Completed',
'team_member': 'John Doe',
'message': 'The project has been marked as completed.',
},
impersonate_user='noreply@example.com'
)
"""
def __init__(self, base_url: Optional[str] = None, api_key: Optional[str] = None):
"""
Initialize the emailer client.
Args:
base_url: Base URL of the emailer service. Defaults to settings.EMAILER_BASE_URL
api_key: API key for authentication. Defaults to settings.EMAILER_API_KEY
"""
self.base_url = base_url or getattr(
settings, 'EMAILER_BASE_URL', 'https://email.example.com'
)
self.api_key = api_key or getattr(settings, 'EMAILER_API_KEY', '')
self.timeout = 30 # seconds
if not self.api_key:
logger.warning("EMAILER_API_KEY not configured. Email sending will fail.")
def _get_headers(self, impersonate_user: Optional[str] = None) -> Dict[str, str]:
"""
Build request headers with authentication and optional impersonation.
Args:
impersonate_user: Email address to send from (requires domain-wide delegation)
Returns:
Dict of HTTP headers
"""
headers = {
'Content-Type': 'application/json',
'X-API-Key': self.api_key,
}
if impersonate_user:
headers['X-Impersonate-User'] = impersonate_user
return headers
def _handle_response(self, response: requests.Response) -> Dict:
"""
Handle API response and raise appropriate exceptions.
Args:
response: requests Response object
Returns:
Parsed JSON response
Raises:
EmailerServiceError: If the request failed
"""
try:
response.raise_for_status()
return response.json() if response.content else {}
except requests.exceptions.HTTPError as e:
error_detail = "Unknown error"
try:
error_data = response.json()
error_detail = error_data.get('message', error_data.get('error', str(e)))
except:
error_detail = response.text or str(e)
logger.error(
f"Emailer API error: {response.status_code} - {error_detail}",
extra={
'status_code': response.status_code,
'url': response.url,
'error': error_detail
}
)
raise EmailerServiceError(f"Email service error: {error_detail}")
except requests.exceptions.RequestException as e:
logger.error(f"Emailer request failed: {str(e)}")
raise EmailerServiceError(f"Failed to connect to email service: {str(e)}")
def send_email(
self,
to: List[str],
subject: str,
body: str,
cc: Optional[List[str]] = None,
bcc: Optional[List[str]] = None,
impersonate_user: Optional[str] = None,
) -> Dict:
"""
Send a plain email.
Args:
to: List of recipient email addresses
subject: Email subject
body: Email body (plain text)
cc: Optional CC recipients
bcc: Optional BCC recipients
impersonate_user: Email address to send from (requires domain-wide delegation)
Returns:
dict: Response with 'id', 'threadId', and 'labelIds'
Raises:
EmailerServiceError: If the request fails
"""
data = {
'to': to,
'subject': subject,
'body': body,
}
if cc:
data['cc'] = cc
if bcc:
data['bcc'] = bcc
try:
response = requests.post(
f"{self.base_url}/api/v1/emails",
headers=self._get_headers(impersonate_user),
json=data,
timeout=self.timeout
)
return self._handle_response(response)
except Exception as e:
logger.exception("Failed to send email")
raise
def send_template_email(
self,
to: List[str],
template_id: str,
variables: Dict[str, str],
cc: Optional[List[str]] = None,
bcc: Optional[List[str]] = None,
impersonate_user: Optional[str] = None,
) -> Dict:
"""
Send an email using a pre-defined template.
Available templates:
- 'notification': General notifications
Variables: subject, team_member, message
- 'service_scheduled': Service scheduling notifications
Variables: team_member, customer_name, service_date, service_address
- 'project_update': Project status updates
Variables: team_member, project_name, project_status, message
Args:
to: List of recipient email addresses
template_id: Template identifier
variables: Template variables (depends on template)
cc: Optional CC recipients
bcc: Optional BCC recipients
impersonate_user: Email address to send from
Returns:
dict: Response with 'id', 'threadId', and 'labelIds'
Raises:
EmailerServiceError: If the request fails
"""
data = {
'to': to,
'template_id': template_id,
'variables': variables,
}
if cc:
data['cc'] = cc
if bcc:
data['bcc'] = bcc
try:
response = requests.post(
f"{self.base_url}/api/v1/templates/send",
headers=self._get_headers(impersonate_user),
json=data,
timeout=self.timeout
)
result = self._handle_response(response)
logger.info(
f"Template email sent successfully",
extra={
'template_id': template_id,
'recipients': to,
'email_id': result.get('id')
}
)
return result
except Exception as e:
logger.exception(f"Failed to send template email: {template_id}")
raise
def list_templates(self) -> List[str]:
"""
Get list of available email templates.
Returns:
list: List of template IDs
Raises:
EmailerServiceError: If the request fails
"""
try:
response = requests.get(
f"{self.base_url}/api/v1/templates",
headers=self._get_headers(),
timeout=self.timeout
)
return self._handle_response(response)
except Exception as e:
logger.exception("Failed to list templates")
raise
def get_template(self, template_id: str) -> Dict:
"""
Get details of a specific email template.
Args:
template_id: Template identifier
Returns:
dict: Template details including variables
Raises:
EmailerServiceError: If the request fails
"""
try:
response = requests.get(
f"{self.base_url}/api/v1/templates/{template_id}",
headers=self._get_headers(),
timeout=self.timeout
)
return self._handle_response(response)
except Exception as e:
logger.exception(f"Failed to get template: {template_id}")
raise
def health_check(self) -> bool:
"""
Check if the emailer service is healthy.
Returns:
bool: True if service is healthy, False otherwise
"""
try:
response = requests.get(
f"{self.base_url}/health",
timeout=5
)
return response.status_code == 200
except Exception as e:
logger.warning(f"Emailer health check failed: {e}")
return False
# Convenience function for quick access
def get_emailer_client() -> EmailerClient:
"""
Get a configured emailer client instance.
Returns:
EmailerClient: Configured client instance
"""
return EmailerClient()