""" 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()