""" Mail Aggregator SMTP — email sending via SMTP. Extracted from aggregator.py for modularity. """ import logging import smtplib from typing import Optional, List, Dict, Any from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from .mail_db import get_email_account from .models import EmailComposeRequest, EmailSendResult logger = logging.getLogger(__name__) class SMTPConnectionError(Exception): """Raised when SMTP connection fails.""" pass class SMTPMixin: """SMTP-related methods for MailAggregator. Provides SMTP connection testing and email sending. Must be mixed into a class that has ``_credentials_service``. """ async def test_smtp_connection( self, smtp_host: str, smtp_port: int, smtp_ssl: bool, email_address: str, password: str, ) -> tuple: """Test SMTP connection. Returns (success, error).""" try: if smtp_ssl: smtp = smtplib.SMTP_SSL(smtp_host, smtp_port) else: smtp = smtplib.SMTP(smtp_host, smtp_port) smtp.starttls() smtp.login(email_address, password) smtp.quit() return True, None except Exception as e: logger.warning(f"SMTP test failed for {email_address}: {e}") return False, f"SMTP Error: {str(e)}" async def send_email( self, account_id: str, user_id: str, request: EmailComposeRequest, ) -> EmailSendResult: """ Send an email via SMTP. Args: account_id: The account to send from user_id: The user ID request: The compose request with recipients and content Returns: EmailSendResult with success status """ account = await get_email_account(account_id, user_id) if not account: return EmailSendResult(success=False, error="Account not found") # Verify the account_id matches if request.account_id != account_id: return EmailSendResult(success=False, error="Account mismatch") # Get credentials vault_path = account.get("vault_path", "") creds = await self._credentials_service.get_credentials(account_id, vault_path) if not creds: return EmailSendResult(success=False, error="Credentials not found") try: # Create message if request.is_html: msg = MIMEMultipart("alternative") msg.attach(MIMEText(request.body, "html")) else: msg = MIMEText(request.body, "plain") msg["Subject"] = request.subject msg["From"] = account["email"] msg["To"] = ", ".join(request.to) if request.cc: msg["Cc"] = ", ".join(request.cc) if request.reply_to_message_id: msg["In-Reply-To"] = request.reply_to_message_id msg["References"] = request.reply_to_message_id # Send via SMTP if account["smtp_ssl"]: smtp = smtplib.SMTP_SSL(account["smtp_host"], account["smtp_port"]) else: smtp = smtplib.SMTP(account["smtp_host"], account["smtp_port"]) smtp.starttls() smtp.login(creds.email, creds.password) # All recipients all_recipients = list(request.to) if request.cc: all_recipients.extend(request.cc) if request.bcc: all_recipients.extend(request.bcc) smtp.sendmail(account["email"], all_recipients, msg.as_string()) smtp.quit() return EmailSendResult( success=True, message_id=msg.get("Message-ID"), ) except Exception as e: logger.error(f"Failed to send email: {e}") return EmailSendResult(success=False, error=str(e))