"""SES email client for sending store onboarding emails.""" import logging import boto3 from botocore.config import Config logger = logging.getLogger(__name__) # SES enforces per-account sending-rate limits, so Throttling is an expected # transient error under batch sends — retry it with backoff rather than # failing the whole batch. "standard" mode also retries connection/timeout # errors and other transient 5xx-class failures. SES_CONNECT_TIMEOUT_SECONDS = 10 SES_READ_TIMEOUT_SECONDS = 30 SES_MAX_RETRY_ATTEMPTS = 4 class SESClient: """Wrapper for AWS SES email sending.""" def __init__(self, region: str = "us-east-1"): self.client = boto3.client( "ses", region_name=region, config=Config( connect_timeout=SES_CONNECT_TIMEOUT_SECONDS, read_timeout=SES_READ_TIMEOUT_SECONDS, retries={"max_attempts": SES_MAX_RETRY_ATTEMPTS, "mode": "standard"}, ), ) self.region = region def send_email( self, to_addresses: list[str], subject: str, body_text: str, from_address: str, source_arn: str | None = None, ) -> str: """Send email via SES. Args: to_addresses: List of recipient email addresses subject: Email subject line body_text: Plain text email body from_address: Sender email address. Must be verified in SES, either directly or via the identity referenced by source_arn if from_address belongs to a different account source_arn: ARN of the identity (owned by the SES account) whose sending-authorization policy permits sending as from_address, when from_address belongs to a different account (SES sending authorization) Returns: Message ID from SES """ try: # BCC, not To: these are individual contacts, not a shared distro, # so putting them all in To would expose each contact's address # to every other recipient of the same email. kwargs = { "Source": from_address, "Destination": {"BccAddresses": to_addresses}, "Message": { "Subject": {"Data": subject, "Charset": "UTF-8"}, "Body": {"Text": {"Data": body_text, "Charset": "UTF-8"}}, }, } if source_arn: kwargs["SourceArn"] = source_arn response = self.client.send_email(**kwargs) message_id = response["MessageId"] logger.info(f"Email sent: message_id={message_id}, recipient_count={len(to_addresses)}, subject={subject}") return message_id except Exception as exc: logger.error(f"Failed to send email ({len(to_addresses)} recipient(s)): {exc}", exc_info=True) raise