From 6823f86256fddc5b246ea19a322eb9c640c2a821 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Thu, 9 Apr 2026 10:36:16 -0500 Subject: [PATCH 01/25] Initial commit of codeloom code --- .doc_gen/metadata/sesv2_metadata.yaml | 89 ++++ .gitignore | 1 + python/example_code/sesv2/requirements.txt | 3 +- .../sesv2/scenario_sesv2_email_attachments.py | 396 ++++++++++++++++++ python/example_code/sesv2/sesv2_hello.py | 77 ++++ python/example_code/sesv2/sesv2_wrapper.py | 352 ++++++++++++++++ .../sesv2/test_sesv2_email_attachments.py | 210 ++++++++++ 7 files changed, 1127 insertions(+), 1 deletion(-) create mode 100644 python/example_code/sesv2/scenario_sesv2_email_attachments.py create mode 100644 python/example_code/sesv2/sesv2_hello.py create mode 100644 python/example_code/sesv2/sesv2_wrapper.py create mode 100644 python/example_code/sesv2/test_sesv2_email_attachments.py diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 313a51113c9..45b53bca279 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -101,6 +101,15 @@ sesv2_CreateContact: sesv2: {CreateContact} sesv2_GetEmailIdentity: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.GetEmailIdentity Rust: versions: - sdk_version: 1 @@ -248,6 +257,10 @@ sesv2_SendEmail: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.template + - description: Sends a message with optional attachments. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SendEmail services: sesv2: {SendEmail} sesv2_CreateEmailIdentity: @@ -290,6 +303,10 @@ sesv2_CreateEmailIdentity: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailIdentity + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.CreateEmailIdentity Rust: versions: - sdk_version: 1 @@ -341,6 +358,10 @@ sesv2_CreateEmailTemplate: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailTemplate + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.CreateEmailTemplate Rust: versions: - sdk_version: 1 @@ -443,6 +464,10 @@ sesv2_DeleteEmailIdentity: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailIdentity + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.DeleteEmailIdentity Rust: versions: - sdk_version: 1 @@ -494,6 +519,10 @@ sesv2_DeleteEmailTemplate: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailTemplate + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.DeleteEmailTemplate Rust: versions: - sdk_version: 1 @@ -581,3 +610,63 @@ sesv2_NewsletterWorkflow: services: sesv2: {CreateContactList, CreateContact, ListContacts, SendEmail.simple, SendEmail.template, CreateEmailIdentity, CreateEmailTemplate, DeleteContactList, DeleteEmailIdentity, DeleteEmailTemplate} + +sesv2_Hello: + title: Hello &SESv2; + title_abbrev: Hello &SESv2; + synopsis: get started using &SESv2;. + category: Hello + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.Hello + services: + sesv2: {ListEmailIdentities} +sesv2_SendBulkEmail: + title: Send bulk email with &SESv2; + title_abbrev: Send bulk email + synopsis: send bulk email with &SESv2;. + category: Actions + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SendBulkEmail + services: + sesv2: {SendBulkEmail} +sesv2_Scenario_EmailAttachments: + title: Send emails with attachments using &SESv2; + title_abbrev: Email Attachments Scenario + synopsis: send emails with attachments using &SESv2;. + synopsis_list: + - Verify sender email identity. + - Create an email template for bulk sends. + - Send a simple email with a file attachment. + - Send a simple email with an inline image. + - Send bulk templated emails with attachments. + - Clean up resources. + category: Scenarios + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: Create an SESv2 wrapper class to manage operations. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.class + - description: Run an interactive scenario demonstrating email attachments. + snippet_tags: + - python.example_code.sesv2.Scenario_EmailAttachments + services: + sesv2: {SendEmail, SendBulkEmail, CreateEmailIdentity, CreateEmailTemplate, GetEmailIdentity, DeleteEmailTemplate, DeleteEmailIdentity} diff --git a/.gitignore b/.gitignore index 657ad41f507..f9318473e2b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ kotlin/services/**/.kotlin/ .kiro/settings/ .kiro/steering/ +/codeloom_outputs diff --git a/python/example_code/sesv2/requirements.txt b/python/example_code/sesv2/requirements.txt index 621e276912d..1ce66d6ec91 100644 --- a/python/example_code/sesv2/requirements.txt +++ b/python/example_code/sesv2/requirements.txt @@ -1,2 +1,3 @@ -boto3>=1.26.79 +boto3>=1.35.0 +botocore>=1.35.0 pytest>=7.2.1 diff --git a/python/example_code/sesv2/scenario_sesv2_email_attachments.py b/python/example_code/sesv2/scenario_sesv2_email_attachments.py new file mode 100644 index 00000000000..198d35275e2 --- /dev/null +++ b/python/example_code/sesv2/scenario_sesv2_email_attachments.py @@ -0,0 +1,396 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Purpose + Shows how to use the AWS SDK for Python (Boto3) with Amazon SESv2 to + send emails with attachments. This scenario demonstrates three use cases: + 1. Send a simple email with a file attachment. + 2. Send a simple email with an inline image rendered in the HTML body. + 3. Send bulk templated emails with attachments to multiple recipients. + + The new attachment support eliminates the need for developers to construct + raw MIME messages — SES handles the MIME assembly automatically. +""" + +import json +import logging +import sys + +import boto3 +from botocore.exceptions import ClientError + +from sesv2_wrapper import SESv2Wrapper + +# Add relative path to include demo_tools in this code example without need for setup. +sys.path.append("../..") +import demo_tools.question as q # noqa + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.sesv2.Scenario_EmailAttachments] +class SESv2EmailAttachmentsScenario: + """ + Demonstrates sending emails with attachments using Amazon SESv2. + + This scenario walks through: + 1. Setting up an email identity and template. + 2. Sending a simple email with a file attachment. + 3. Sending a simple email with an inline image. + 4. Sending bulk templated emails with attachments. + 5. Cleaning up created resources. + """ + + TEMPLATE_NAME = "AttachmentDemoTemplate" + + def __init__(self, sesv2_wrapper: SESv2Wrapper) -> None: + """ + :param sesv2_wrapper: An instance of the SESv2Wrapper class. + """ + self.sesv2_wrapper = sesv2_wrapper + self.sender_email = "" + self.recipient_emails: list = [] + self.identity_was_created = False + + def run_scenario(self) -> None: + """Runs the SESv2 email attachments scenario.""" + print("-" * 88) + print("Welcome to the Amazon SESv2 Email Attachments Scenario!") + print("-" * 88) + print( + "This scenario demonstrates how to send emails with attachments\n" + "using the new SESv2 attachment support. SES handles MIME\n" + "construction automatically, so you don't need to build raw\n" + "MIME messages.\n" + ) + + try: + self._setup() + self._step1_send_email_with_attachment() + self._step2_send_email_with_inline_image() + self._step3_send_bulk_email_with_attachments() + except Exception as e: + logger.error("Scenario failed: %s", e) + print(f"\nThe scenario encountered an error: {e}") + finally: + self._cleanup() + + # ---------- Setup ---------- + + def _setup(self) -> None: + """ + Prompts for configuration, verifies the sender identity, prepares a + sample attachment, and creates an email template. + """ + print("\n--- Setup ---\n") + + # Prompt for sender and recipient addresses. + print( + "Both sender and recipient addresses must be verified if your\n" + "account is in the SES sandbox.\n" + ) + self.sender_email = q.ask( + "Enter a verified sender email address: " + ) + recipient_input = q.ask( + "Enter one or more recipient email addresses (comma-separated): " + ) + self.recipient_emails = [ + addr.strip() for addr in recipient_input.split(",") if addr.strip() + ] + + # Verify the sender identity. + print(f"\nChecking identity for {self.sender_email}...") + try: + identity_info = self.sesv2_wrapper.get_email_identity( + self.sender_email + ) + verified = identity_info.get("VerifiedForSendingStatus", False) + if verified: + print(f" ✓ {self.sender_email} is verified and ready to send.") + else: + print( + f" ⚠ {self.sender_email} exists but is not yet verified." + ) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + print( + f" Identity {self.sender_email} not found. " + "Creating it now..." + ) + result = self.sesv2_wrapper.create_email_identity( + self.sender_email + ) + self.identity_was_created = True + print( + f" Identity created. Verification status: " + f"{result.get('VerifiedForSendingStatus', False)}" + ) + print( + " Check your inbox and click the verification link " + "before continuing." + ) + q.ask("Press Enter when you have verified the address...") + else: + raise + + # Create the email template for the bulk-send step. + print("\nCreating email template for the bulk email step...") + try: + self.sesv2_wrapper.create_email_template( + template_name=self.TEMPLATE_NAME, + subject="Bulk Email with Attachment for {{name}}", + html_body=( + "

Hello {{name}}

" + "

Please find the attached document.

" + ), + text_body=( + "Hello {{name}}, Please find the attached document." + ), + ) + print(f" ✓ Template '{self.TEMPLATE_NAME}' created.\n") + except ClientError as err: + if err.response["Error"]["Code"] == "AlreadyExistsException": + print( + f" Template '{self.TEMPLATE_NAME}' already exists. " + "Using it.\n" + ) + else: + raise + + # ---------- Step 1: Simple email with file attachment ---------- + + def _step1_send_email_with_attachment(self) -> None: + """Sends a simple email with a text file attachment.""" + print("\n--- Step 1: Send a Simple Email with a File Attachment ---\n") + print( + "Creating a sample text file attachment and sending it with\n" + "the Simple email content type. SES constructs the MIME message\n" + "automatically.\n" + ) + + # Prepare a sample text file as bytes. + sample_content = b"This is a sample report attachment." + + attachment = { + "RawContent": sample_content, + "FileName": "sample-report.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Sample report text file", + "ContentTransferEncoding": "BASE64", + } + + print( + "Note: When using an AWS SDK, the SDK handles base64 encoding\n" + "automatically. Direct API callers must encode content themselves.\n" + ) + + message_id = self.sesv2_wrapper.send_email( + from_address=self.sender_email, + to_addresses=self.recipient_emails, + subject="SESv2 Attachment Demo — Simple Email with Attachment", + html_body=( + "

Attachment Demo

" + "

Please see the attached report document.

" + ), + text_body="Please see the attached report document.", + attachments=[attachment], + ) + + print(f" ✓ Email sent! MessageId: {message_id}") + print( + " SES automatically constructed the MIME message with the " + "attachment.\n" + ) + + # ---------- Step 2: Simple email with inline image ---------- + + def _step2_send_email_with_inline_image(self) -> None: + """Sends a simple email with an inline image that renders in HTML.""" + print("\n--- Step 2: Send a Simple Email with an Inline Image ---\n") + print( + "This step demonstrates INLINE disposition. The image renders\n" + "directly in the HTML body using a 'cid:' reference instead of\n" + "appearing as a downloadable attachment.\n" + ) + + # Create a minimal 1x1 red PNG (valid PNG file). + sample_image = ( + b"\x89PNG\r\n\x1a\n" # PNG signature + b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01" + b"\x08\x02\x00\x00\x00\x90wS\xde" # 1x1 RGB + b"\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01" + b"\x00\x05\x18\xd8N" # compressed data + b"\x00\x00\x00\x00IEND\xaeB`\x82" # IEND + ) + + attachment = { + "RawContent": sample_image, + "FileName": "logo.png", + "ContentType": "image/png", + "ContentDisposition": "INLINE", + "ContentId": "logo123", + "ContentDescription": "Company logo", + "ContentTransferEncoding": "BASE64", + } + + html_body = ( + "" + "

Inline Image Demo

" + "

Here is our logo:

" + 'Company Logo' + "" + ) + + message_id = self.sesv2_wrapper.send_email( + from_address=self.sender_email, + to_addresses=self.recipient_emails, + subject="SESv2 Attachment Demo — Inline Image", + html_body=html_body, + text_body=( + "Inline Image Demo — Please view this email in an " + "HTML-capable client to see the embedded image." + ), + attachments=[attachment], + ) + + print(f" ✓ Email sent! MessageId: {message_id}") + print( + " The ContentId 'logo123' is referenced in the HTML body via\n" + " 'cid:logo123', which lets the image render inline.\n" + ) + + # ---------- Step 3: Bulk templated email with attachments ---------- + + def _step3_send_bulk_email_with_attachments(self) -> None: + """Sends bulk templated emails with attachments to multiple recipients.""" + print("\n--- Step 3: Send Bulk Templated Emails with Attachments ---\n") + print( + "Using SendBulkEmail to send a templated email with an attachment\n" + "to multiple recipients in a single API call. Each recipient gets\n" + "personalized content via template data.\n" + ) + + sample_content = b"This is a sample report attachment." + + attachment = { + "RawContent": sample_content, + "FileName": "sample-report.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Sample report for bulk recipients", + "ContentTransferEncoding": "BASE64", + } + + # Build one entry per recipient with personalized names. + names = ["Alice", "Bob", "Charlie", "Diana", "Eve"] + bulk_entries = [] + for i, email in enumerate(self.recipient_emails): + name = names[i] if i < len(names) else f"Recipient{i + 1}" + bulk_entries.append( + { + "Destination": {"ToAddresses": [email]}, + "ReplacementEmailContent": { + "ReplacementTemplate": { + "ReplacementTemplateData": json.dumps( + {"name": name} + ) + } + }, + } + ) + + results = self.sesv2_wrapper.send_bulk_email( + from_address=self.sender_email, + template_name=self.TEMPLATE_NAME, + default_template_data='{"name": "Valued Customer"}', + bulk_entries=bulk_entries, + attachments=[attachment], + ) + + print(" Bulk email results:") + for idx, result in enumerate(results): + status = result.get("Status", "Unknown") + msg_id = result.get("MessageId", "N/A") + error = result.get("Error", "") + recipient = ( + self.recipient_emails[idx] + if idx < len(self.recipient_emails) + else "Unknown" + ) + print(f" {recipient}: Status={status}, MessageId={msg_id}") + if error: + print(f" Error: {error}") + + print( + "\n All recipients receive the same attachment(s) defined in\n" + " DefaultContent. Template data is personalized per recipient.\n" + ) + + # ---------- Cleanup ---------- + + def _cleanup(self) -> None: + """Deletes the email template and optionally the email identity.""" + print("\n--- Cleanup ---\n") + + # Delete the email template. + try: + self.sesv2_wrapper.delete_email_template(self.TEMPLATE_NAME) + print(f" ✓ Template '{self.TEMPLATE_NAME}' deleted.") + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + print( + f" Template '{self.TEMPLATE_NAME}' was already deleted." + ) + else: + logger.error("Failed to delete template: %s", err) + + # Optionally delete the email identity. + if self.identity_was_created and self.sender_email: + delete_identity = q.ask( + f"Delete the email identity '{self.sender_email}'? (y/n) ", + q.is_yesno, + ) + if delete_identity: + try: + self.sesv2_wrapper.delete_email_identity( + self.sender_email + ) + print( + f" ✓ Email identity '{self.sender_email}' deleted." + ) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + print( + f" Identity '{self.sender_email}' was " + "already deleted." + ) + else: + logger.error( + "Failed to delete identity: %s", err + ) + else: + print( + f" Skipping identity deletion for {self.sender_email}." + ) + else: + print( + " Sender identity was pre-existing. Skipping deletion." + ) + + print("\nAll resources have been cleaned up.") + print("-" * 88) + + +# snippet-end:[python.example_code.sesv2.Scenario_EmailAttachments] + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + try: + scenario = SESv2EmailAttachmentsScenario(SESv2Wrapper.from_client()) + scenario.run_scenario() + except Exception: + logging.exception("Something went wrong with the scenario.") \ No newline at end of file diff --git a/python/example_code/sesv2/sesv2_hello.py b/python/example_code/sesv2/sesv2_hello.py new file mode 100644 index 00000000000..c707c92d3a9 --- /dev/null +++ b/python/example_code/sesv2/sesv2_hello.py @@ -0,0 +1,77 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Purpose + Shows how to get started with Amazon SESv2 by listing email identities + associated with the account. +""" + +import logging + +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.sesv2.Hello] +def hello_sesv2(sesv2_client): + """ + Use the AWS SDK for Python (Boto3) to create an Amazon SESv2 client and + list the email identities in your account. This example uses the default + settings specified in your shared credentials and config files. + + :param sesv2_client: A Boto3 SESv2 client object. + """ + print("Hello, Amazon SESv2! Let's list your email identities:\n") + + identity_count = 0 + next_token = None + try: + while True: + kwargs = {"PageSize": 20} + if next_token: + kwargs["NextToken"] = next_token + response = sesv2_client.list_email_identities(**kwargs) + identities = response.get("EmailIdentities", []) + for identity in identities: + identity_count += 1 + identity_name = identity.get("IdentityName", "Unknown") + identity_type = identity.get("IdentityType", "Unknown") + verification_status = identity.get( + "VerificationStatus", "Unknown" + ) + sending_enabled = identity.get("SendingEnabled", False) + print( + f" Identity: {identity_name}" + f" Type: {identity_type}" + f" Status: {verification_status}" + f" Sending: {'Enabled' if sending_enabled else 'Disabled'}" + ) + next_token = response.get("NextToken") + if not next_token: + break + + if identity_count == 0: + print( + "No email identities found. " + "Use CreateEmailIdentity to add one." + ) + else: + print(f"\nFound {identity_count} email identity(ies).") + + except ClientError as err: + logger.error( + "Couldn't list email identities. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + +# snippet-end:[python.example_code.sesv2.Hello] + + +if __name__ == "__main__": + hello_sesv2(boto3.client("sesv2")) diff --git a/python/example_code/sesv2/sesv2_wrapper.py b/python/example_code/sesv2/sesv2_wrapper.py new file mode 100644 index 00000000000..870314325b7 --- /dev/null +++ b/python/example_code/sesv2/sesv2_wrapper.py @@ -0,0 +1,352 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Encapsulates Amazon SESv2 actions for sending emails with attachments. +""" + +import logging +from typing import Any, Dict, List, Optional + +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.sesv2.SESv2Wrapper.class] +# snippet-start:[python.example_code.sesv2.SESv2Wrapper.decl] +class SESv2Wrapper: + """Encapsulates Amazon SESv2 email sending actions.""" + + def __init__(self, sesv2_client: Any) -> None: + """ + Initializes the SESv2Wrapper with an SESv2 client. + + :param sesv2_client: A Boto3 SESv2 client. + """ + self.sesv2_client = sesv2_client + + @classmethod + def from_client(cls) -> "SESv2Wrapper": + """ + Creates an SESv2Wrapper instance with a default Boto3 SESv2 client. + + :return: A new SESv2Wrapper instance. + """ + sesv2_client = boto3.client("sesv2") + return cls(sesv2_client) + + # snippet-end:[python.example_code.sesv2.SESv2Wrapper.decl] + + # snippet-start:[python.example_code.sesv2.GetEmailIdentity] + def get_email_identity(self, email_address: str) -> Dict[str, Any]: + """ + Gets information about an email identity, including its verification status. + + :param email_address: The email address or domain to look up. + :return: A dictionary with identity information including verification status. + :raises ClientError: If the identity is not found (NotFoundException). + """ + try: + response = self.sesv2_client.get_email_identity( + EmailIdentity=email_address + ) + logger.info("Got email identity for %s.", email_address) + return response + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + logger.info( + "Email identity %s not found.", email_address + ) + else: + logger.error( + "Couldn't get email identity %s. Here's why: %s: %s", + email_address, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.GetEmailIdentity] + + # snippet-start:[python.example_code.sesv2.CreateEmailIdentity] + def create_email_identity(self, email_address: str) -> Dict[str, Any]: + """ + Starts the process of verifying an email identity (email address or domain). + + :param email_address: The email address or domain to verify. + :return: A dictionary with the identity type and verification status. + :raises ClientError: If the limit is exceeded (LimitExceededException). + """ + try: + response = self.sesv2_client.create_email_identity( + EmailIdentity=email_address + ) + logger.info( + "Started verification for email identity %s.", email_address + ) + return response + except ClientError as err: + if err.response["Error"]["Code"] == "LimitExceededException": + logger.error( + "Couldn't create email identity %s. You have exceeded " + "the maximum number of email identities. " + "Use an existing verified identity.", + email_address, + ) + else: + logger.error( + "Couldn't create email identity %s. Here's why: %s: %s", + email_address, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.CreateEmailIdentity] + + # snippet-start:[python.example_code.sesv2.CreateEmailTemplate] + def create_email_template( + self, + template_name: str, + subject: str, + html_body: str, + text_body: str, + ) -> None: + """ + Creates an email template for use with templated and bulk email sends. + + :param template_name: The name for the new template. + :param subject: The subject line of the template. May include {{placeholders}}. + :param html_body: The HTML body of the template. + :param text_body: The plain text body of the template. + :raises ClientError: If the template limit is exceeded (LimitExceededException). + """ + try: + self.sesv2_client.create_email_template( + TemplateName=template_name, + TemplateContent={ + "Subject": subject, + "Html": html_body, + "Text": text_body, + }, + ) + logger.info("Created email template %s.", template_name) + except ClientError as err: + if err.response["Error"]["Code"] == "LimitExceededException": + logger.error( + "Couldn't create email template %s. You have exceeded " + "the maximum number of email templates. " + "Delete unused templates first.", + template_name, + ) + else: + logger.error( + "Couldn't create email template %s. Here's why: %s: %s", + template_name, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.CreateEmailTemplate] + + # snippet-start:[python.example_code.sesv2.SendEmail] + def send_email( + self, + from_address: str, + to_addresses: List[str], + subject: str, + html_body: str, + text_body: str, + attachments: Optional[List[Dict[str, Any]]] = None, + ) -> str: + """ + Sends a simple email message with optional attachments. + + SES handles MIME construction automatically when using attachments + with the Simple content type, so developers don't need to build + raw MIME messages. + + :param from_address: The verified sender email address. + :param to_addresses: A list of recipient email addresses. + :param subject: The subject line of the email. + :param html_body: The HTML body content. + :param text_body: The plain text body content. + :param attachments: An optional list of attachment dictionaries. Each + attachment should contain 'RawContent' (bytes), 'FileName' (str), + and optionally 'ContentType', 'ContentDisposition', 'ContentId', + 'ContentDescription', and 'ContentTransferEncoding'. + :return: The MessageId of the sent email. + :raises ClientError: If the message is rejected (MessageRejected). + """ + try: + simple_message: Dict[str, Any] = { + "Subject": {"Data": subject}, + "Body": { + "Html": {"Data": html_body}, + "Text": {"Data": text_body}, + }, + } + + if attachments: + simple_message["Attachments"] = attachments + + response = self.sesv2_client.send_email( + FromEmailAddress=from_address, + Destination={"ToAddresses": to_addresses}, + Content={"Simple": simple_message}, + ) + message_id = response["MessageId"] + logger.info( + "Sent email from %s to %s. MessageId: %s", + from_address, + to_addresses, + message_id, + ) + return message_id + except ClientError as err: + if err.response["Error"]["Code"] == "MessageRejected": + logger.error( + "Message was rejected. Check that attachments use " + "supported file types and total message size is " + "under 40 MB. Details: %s", + err.response["Error"]["Message"], + ) + else: + logger.error( + "Couldn't send email. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.SendEmail] + + # snippet-start:[python.example_code.sesv2.SendBulkEmail] + def send_bulk_email( + self, + from_address: str, + template_name: str, + default_template_data: str, + bulk_entries: List[Dict[str, Any]], + attachments: Optional[List[Dict[str, Any]]] = None, + ) -> List[Dict[str, Any]]: + """ + Sends a templated email to multiple recipients in a single API call. + + All recipients receive the same attachment(s) defined in the default + content, while template data can be personalized per recipient. + + :param from_address: The verified sender email address. + :param template_name: The name of an existing email template. + :param default_template_data: Default JSON template data string. + :param bulk_entries: A list of BulkEmailEntry dicts, each containing + 'Destination' and optionally 'ReplacementEmailContent'. + :param attachments: An optional list of attachment dicts for all + recipients. + :return: A list of BulkEmailEntryResult dicts with status and MessageId. + :raises ClientError: If the message is rejected (MessageRejected). + """ + try: + template_content: Dict[str, Any] = { + "TemplateName": template_name, + "TemplateData": default_template_data, + } + + if attachments: + template_content["Attachments"] = attachments + + response = self.sesv2_client.send_bulk_email( + FromEmailAddress=from_address, + DefaultContent={"Template": template_content}, + BulkEmailEntries=bulk_entries, + ) + results = response.get("BulkEmailEntryResults", []) + logger.info( + "Sent bulk email from %s to %d recipients.", + from_address, + len(bulk_entries), + ) + return results + except ClientError as err: + if err.response["Error"]["Code"] == "MessageRejected": + logger.error( + "Bulk message was rejected. Check that the template " + "exists, attachment file types are supported, and " + "total message size is within limits. Details: %s", + err.response["Error"]["Message"], + ) + else: + logger.error( + "Couldn't send bulk email. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.SendBulkEmail] + + # snippet-start:[python.example_code.sesv2.DeleteEmailTemplate] + def delete_email_template(self, template_name: str) -> None: + """ + Deletes an email template. + + :param template_name: The name of the template to delete. + :raises ClientError: If the template is not found (NotFoundException). + """ + try: + self.sesv2_client.delete_email_template( + TemplateName=template_name + ) + logger.info("Deleted email template %s.", template_name) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + logger.info( + "Email template %s not found or already deleted.", + template_name, + ) + else: + logger.error( + "Couldn't delete email template %s. Here's why: %s: %s", + template_name, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.DeleteEmailTemplate] + + # snippet-start:[python.example_code.sesv2.DeleteEmailIdentity] + def delete_email_identity(self, email_address: str) -> None: + """ + Deletes an email identity. + + :param email_address: The email address or domain to delete. + :raises ClientError: If the identity is not found (NotFoundException). + """ + try: + self.sesv2_client.delete_email_identity( + EmailIdentity=email_address + ) + logger.info("Deleted email identity %s.", email_address) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + logger.info( + "Email identity %s not found or already deleted.", + email_address, + ) + else: + logger.error( + "Couldn't delete email identity %s. Here's why: %s: %s", + email_address, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.DeleteEmailIdentity] + + +# snippet-end:[python.example_code.sesv2.SESv2Wrapper.class] \ No newline at end of file diff --git a/python/example_code/sesv2/test_sesv2_email_attachments.py b/python/example_code/sesv2/test_sesv2_email_attachments.py new file mode 100644 index 00000000000..71b638d68d1 --- /dev/null +++ b/python/example_code/sesv2/test_sesv2_email_attachments.py @@ -0,0 +1,210 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Integration tests for the SESv2 Email Attachments scenario. + +These tests call real AWS SESv2 APIs and require a verified email identity. +Set the environment variable SENDER_EMAIL to a verified SES email address. +""" + +import os + +import boto3 +import pytest + +from sesv2_wrapper import SESv2Wrapper +from scenario_sesv2_email_attachments import SESv2EmailAttachmentsScenario + + +@pytest.mark.integ +def test_sesv2_wrapper_get_email_identity(): + """Test that get_email_identity returns identity info for a verified sender.""" + sender_email = os.environ.get("SENDER_EMAIL") + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + try: + result = wrapper.get_email_identity(sender_email) + assert "VerifiedForSendingStatus" in result + finally: + # No cleanup needed for read-only operation. + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_create_and_delete_template(): + """Test creating and deleting an email template.""" + wrapper = SESv2Wrapper.from_client() + template_name = "IntegTestTemplate" + try: + wrapper.create_email_template( + template_name=template_name, + subject="Test Subject for {{name}}", + html_body="

Hello {{name}}

", + text_body="Hello {{name}}", + ) + finally: + try: + wrapper.delete_email_template(template_name) + except Exception: + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_send_email_with_attachment(): + """Test sending a simple email with a file attachment.""" + sender_email = os.environ.get("SENDER_EMAIL") + recipient_email = os.environ.get("RECIPIENT_EMAIL", sender_email) + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + sample_content = b"Integration test attachment content." + + attachment = { + "RawContent": sample_content, + "FileName": "integ-test.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Integration test file", + "ContentTransferEncoding": "BASE64", + } + + try: + message_id = wrapper.send_email( + from_address=sender_email, + to_addresses=[recipient_email], + subject="SESv2 Integration Test — Attachment", + html_body="

Integration test with attachment.

", + text_body="Integration test with attachment.", + attachments=[attachment], + ) + assert message_id is not None + assert len(message_id) > 0 + finally: + # No cleanup needed for sent emails. + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_send_email_with_inline_image(): + """Test sending a simple email with an inline image.""" + sender_email = os.environ.get("SENDER_EMAIL") + recipient_email = os.environ.get("RECIPIENT_EMAIL", sender_email) + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + + # Minimal 1x1 PNG + sample_image = ( + b"\x89PNG\r\n\x1a\n" + b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01" + b"\x08\x02\x00\x00\x00\x90wS\xde" + b"\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01" + b"\x00\x05\x18\xd8N" + b"\x00\x00\x00\x00IEND\xaeB`\x82" + ) + + attachment = { + "RawContent": sample_image, + "FileName": "test-logo.png", + "ContentType": "image/png", + "ContentDisposition": "INLINE", + "ContentId": "testlogo123", + "ContentDescription": "Test logo", + "ContentTransferEncoding": "BASE64", + } + + html_body = ( + '

Test

' + 'Test Logo' + '' + ) + + try: + message_id = wrapper.send_email( + from_address=sender_email, + to_addresses=[recipient_email], + subject="SESv2 Integration Test — Inline Image", + html_body=html_body, + text_body="Inline image test — view in HTML client.", + attachments=[attachment], + ) + assert message_id is not None + assert len(message_id) > 0 + finally: + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_send_bulk_email_with_attachment(): + """Test sending bulk templated email with an attachment.""" + sender_email = os.environ.get("SENDER_EMAIL") + recipient_email = os.environ.get("RECIPIENT_EMAIL", sender_email) + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + template_name = "IntegTestBulkTemplate" + + sample_content = b"Bulk test attachment content." + attachment = { + "RawContent": sample_content, + "FileName": "bulk-test.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Bulk test file", + "ContentTransferEncoding": "BASE64", + } + + try: + wrapper.create_email_template( + template_name=template_name, + subject="Bulk Test for {{name}}", + html_body="

Hello {{name}}

Attached document.

", + text_body="Hello {{name}}, attached document.", + ) + + bulk_entries = [ + { + "Destination": {"ToAddresses": [recipient_email]}, + "ReplacementEmailContent": { + "ReplacementTemplate": { + "ReplacementTemplateData": '{"name": "TestUser"}' + } + }, + } + ] + + results = wrapper.send_bulk_email( + from_address=sender_email, + template_name=template_name, + default_template_data='{"name": "Default"}', + bulk_entries=bulk_entries, + attachments=[attachment], + ) + + assert results is not None + assert len(results) == 1 + assert results[0].get("Status") == "SUCCESS" + finally: + try: + wrapper.delete_email_template(template_name) + except Exception: + pass + + +@pytest.mark.integ +def test_sesv2_hello(capsys): + """Test the Hello SESv2 example.""" + from sesv2_hello import hello_sesv2 + + try: + hello_sesv2(boto3.client("sesv2")) + captured = capsys.readouterr() + assert "Hello, Amazon SESv2!" in captured.out + finally: + pass \ No newline at end of file From 637d63a62f8d0382304e4f1d6e8a649327582fd9 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Thu, 9 Apr 2026 10:36:16 -0500 Subject: [PATCH 02/25] Initial commit of codeloom code --- .doc_gen/metadata/sesv2_metadata.yaml | 89 ++++ .gitignore | 1 + python/example_code/sesv2/requirements.txt | 3 +- .../sesv2/scenario_sesv2_email_attachments.py | 396 ++++++++++++++++++ python/example_code/sesv2/sesv2_hello.py | 77 ++++ python/example_code/sesv2/sesv2_wrapper.py | 352 ++++++++++++++++ .../sesv2/test_sesv2_email_attachments.py | 210 ++++++++++ 7 files changed, 1127 insertions(+), 1 deletion(-) create mode 100644 python/example_code/sesv2/scenario_sesv2_email_attachments.py create mode 100644 python/example_code/sesv2/sesv2_hello.py create mode 100644 python/example_code/sesv2/sesv2_wrapper.py create mode 100644 python/example_code/sesv2/test_sesv2_email_attachments.py diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 313a51113c9..45b53bca279 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -101,6 +101,15 @@ sesv2_CreateContact: sesv2: {CreateContact} sesv2_GetEmailIdentity: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.GetEmailIdentity Rust: versions: - sdk_version: 1 @@ -248,6 +257,10 @@ sesv2_SendEmail: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.template + - description: Sends a message with optional attachments. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SendEmail services: sesv2: {SendEmail} sesv2_CreateEmailIdentity: @@ -290,6 +303,10 @@ sesv2_CreateEmailIdentity: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailIdentity + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.CreateEmailIdentity Rust: versions: - sdk_version: 1 @@ -341,6 +358,10 @@ sesv2_CreateEmailTemplate: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailTemplate + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.CreateEmailTemplate Rust: versions: - sdk_version: 1 @@ -443,6 +464,10 @@ sesv2_DeleteEmailIdentity: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailIdentity + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.DeleteEmailIdentity Rust: versions: - sdk_version: 1 @@ -494,6 +519,10 @@ sesv2_DeleteEmailTemplate: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailTemplate + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.DeleteEmailTemplate Rust: versions: - sdk_version: 1 @@ -581,3 +610,63 @@ sesv2_NewsletterWorkflow: services: sesv2: {CreateContactList, CreateContact, ListContacts, SendEmail.simple, SendEmail.template, CreateEmailIdentity, CreateEmailTemplate, DeleteContactList, DeleteEmailIdentity, DeleteEmailTemplate} + +sesv2_Hello: + title: Hello &SESv2; + title_abbrev: Hello &SESv2; + synopsis: get started using &SESv2;. + category: Hello + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.Hello + services: + sesv2: {ListEmailIdentities} +sesv2_SendBulkEmail: + title: Send bulk email with &SESv2; + title_abbrev: Send bulk email + synopsis: send bulk email with &SESv2;. + category: Actions + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SendBulkEmail + services: + sesv2: {SendBulkEmail} +sesv2_Scenario_EmailAttachments: + title: Send emails with attachments using &SESv2; + title_abbrev: Email Attachments Scenario + synopsis: send emails with attachments using &SESv2;. + synopsis_list: + - Verify sender email identity. + - Create an email template for bulk sends. + - Send a simple email with a file attachment. + - Send a simple email with an inline image. + - Send bulk templated emails with attachments. + - Clean up resources. + category: Scenarios + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: Create an SESv2 wrapper class to manage operations. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.class + - description: Run an interactive scenario demonstrating email attachments. + snippet_tags: + - python.example_code.sesv2.Scenario_EmailAttachments + services: + sesv2: {SendEmail, SendBulkEmail, CreateEmailIdentity, CreateEmailTemplate, GetEmailIdentity, DeleteEmailTemplate, DeleteEmailIdentity} diff --git a/.gitignore b/.gitignore index 657ad41f507..f9318473e2b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ kotlin/services/**/.kotlin/ .kiro/settings/ .kiro/steering/ +/codeloom_outputs diff --git a/python/example_code/sesv2/requirements.txt b/python/example_code/sesv2/requirements.txt index 621e276912d..1ce66d6ec91 100644 --- a/python/example_code/sesv2/requirements.txt +++ b/python/example_code/sesv2/requirements.txt @@ -1,2 +1,3 @@ -boto3>=1.26.79 +boto3>=1.35.0 +botocore>=1.35.0 pytest>=7.2.1 diff --git a/python/example_code/sesv2/scenario_sesv2_email_attachments.py b/python/example_code/sesv2/scenario_sesv2_email_attachments.py new file mode 100644 index 00000000000..198d35275e2 --- /dev/null +++ b/python/example_code/sesv2/scenario_sesv2_email_attachments.py @@ -0,0 +1,396 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Purpose + Shows how to use the AWS SDK for Python (Boto3) with Amazon SESv2 to + send emails with attachments. This scenario demonstrates three use cases: + 1. Send a simple email with a file attachment. + 2. Send a simple email with an inline image rendered in the HTML body. + 3. Send bulk templated emails with attachments to multiple recipients. + + The new attachment support eliminates the need for developers to construct + raw MIME messages — SES handles the MIME assembly automatically. +""" + +import json +import logging +import sys + +import boto3 +from botocore.exceptions import ClientError + +from sesv2_wrapper import SESv2Wrapper + +# Add relative path to include demo_tools in this code example without need for setup. +sys.path.append("../..") +import demo_tools.question as q # noqa + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.sesv2.Scenario_EmailAttachments] +class SESv2EmailAttachmentsScenario: + """ + Demonstrates sending emails with attachments using Amazon SESv2. + + This scenario walks through: + 1. Setting up an email identity and template. + 2. Sending a simple email with a file attachment. + 3. Sending a simple email with an inline image. + 4. Sending bulk templated emails with attachments. + 5. Cleaning up created resources. + """ + + TEMPLATE_NAME = "AttachmentDemoTemplate" + + def __init__(self, sesv2_wrapper: SESv2Wrapper) -> None: + """ + :param sesv2_wrapper: An instance of the SESv2Wrapper class. + """ + self.sesv2_wrapper = sesv2_wrapper + self.sender_email = "" + self.recipient_emails: list = [] + self.identity_was_created = False + + def run_scenario(self) -> None: + """Runs the SESv2 email attachments scenario.""" + print("-" * 88) + print("Welcome to the Amazon SESv2 Email Attachments Scenario!") + print("-" * 88) + print( + "This scenario demonstrates how to send emails with attachments\n" + "using the new SESv2 attachment support. SES handles MIME\n" + "construction automatically, so you don't need to build raw\n" + "MIME messages.\n" + ) + + try: + self._setup() + self._step1_send_email_with_attachment() + self._step2_send_email_with_inline_image() + self._step3_send_bulk_email_with_attachments() + except Exception as e: + logger.error("Scenario failed: %s", e) + print(f"\nThe scenario encountered an error: {e}") + finally: + self._cleanup() + + # ---------- Setup ---------- + + def _setup(self) -> None: + """ + Prompts for configuration, verifies the sender identity, prepares a + sample attachment, and creates an email template. + """ + print("\n--- Setup ---\n") + + # Prompt for sender and recipient addresses. + print( + "Both sender and recipient addresses must be verified if your\n" + "account is in the SES sandbox.\n" + ) + self.sender_email = q.ask( + "Enter a verified sender email address: " + ) + recipient_input = q.ask( + "Enter one or more recipient email addresses (comma-separated): " + ) + self.recipient_emails = [ + addr.strip() for addr in recipient_input.split(",") if addr.strip() + ] + + # Verify the sender identity. + print(f"\nChecking identity for {self.sender_email}...") + try: + identity_info = self.sesv2_wrapper.get_email_identity( + self.sender_email + ) + verified = identity_info.get("VerifiedForSendingStatus", False) + if verified: + print(f" ✓ {self.sender_email} is verified and ready to send.") + else: + print( + f" ⚠ {self.sender_email} exists but is not yet verified." + ) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + print( + f" Identity {self.sender_email} not found. " + "Creating it now..." + ) + result = self.sesv2_wrapper.create_email_identity( + self.sender_email + ) + self.identity_was_created = True + print( + f" Identity created. Verification status: " + f"{result.get('VerifiedForSendingStatus', False)}" + ) + print( + " Check your inbox and click the verification link " + "before continuing." + ) + q.ask("Press Enter when you have verified the address...") + else: + raise + + # Create the email template for the bulk-send step. + print("\nCreating email template for the bulk email step...") + try: + self.sesv2_wrapper.create_email_template( + template_name=self.TEMPLATE_NAME, + subject="Bulk Email with Attachment for {{name}}", + html_body=( + "

Hello {{name}}

" + "

Please find the attached document.

" + ), + text_body=( + "Hello {{name}}, Please find the attached document." + ), + ) + print(f" ✓ Template '{self.TEMPLATE_NAME}' created.\n") + except ClientError as err: + if err.response["Error"]["Code"] == "AlreadyExistsException": + print( + f" Template '{self.TEMPLATE_NAME}' already exists. " + "Using it.\n" + ) + else: + raise + + # ---------- Step 1: Simple email with file attachment ---------- + + def _step1_send_email_with_attachment(self) -> None: + """Sends a simple email with a text file attachment.""" + print("\n--- Step 1: Send a Simple Email with a File Attachment ---\n") + print( + "Creating a sample text file attachment and sending it with\n" + "the Simple email content type. SES constructs the MIME message\n" + "automatically.\n" + ) + + # Prepare a sample text file as bytes. + sample_content = b"This is a sample report attachment." + + attachment = { + "RawContent": sample_content, + "FileName": "sample-report.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Sample report text file", + "ContentTransferEncoding": "BASE64", + } + + print( + "Note: When using an AWS SDK, the SDK handles base64 encoding\n" + "automatically. Direct API callers must encode content themselves.\n" + ) + + message_id = self.sesv2_wrapper.send_email( + from_address=self.sender_email, + to_addresses=self.recipient_emails, + subject="SESv2 Attachment Demo — Simple Email with Attachment", + html_body=( + "

Attachment Demo

" + "

Please see the attached report document.

" + ), + text_body="Please see the attached report document.", + attachments=[attachment], + ) + + print(f" ✓ Email sent! MessageId: {message_id}") + print( + " SES automatically constructed the MIME message with the " + "attachment.\n" + ) + + # ---------- Step 2: Simple email with inline image ---------- + + def _step2_send_email_with_inline_image(self) -> None: + """Sends a simple email with an inline image that renders in HTML.""" + print("\n--- Step 2: Send a Simple Email with an Inline Image ---\n") + print( + "This step demonstrates INLINE disposition. The image renders\n" + "directly in the HTML body using a 'cid:' reference instead of\n" + "appearing as a downloadable attachment.\n" + ) + + # Create a minimal 1x1 red PNG (valid PNG file). + sample_image = ( + b"\x89PNG\r\n\x1a\n" # PNG signature + b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01" + b"\x08\x02\x00\x00\x00\x90wS\xde" # 1x1 RGB + b"\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01" + b"\x00\x05\x18\xd8N" # compressed data + b"\x00\x00\x00\x00IEND\xaeB`\x82" # IEND + ) + + attachment = { + "RawContent": sample_image, + "FileName": "logo.png", + "ContentType": "image/png", + "ContentDisposition": "INLINE", + "ContentId": "logo123", + "ContentDescription": "Company logo", + "ContentTransferEncoding": "BASE64", + } + + html_body = ( + "" + "

Inline Image Demo

" + "

Here is our logo:

" + 'Company Logo' + "" + ) + + message_id = self.sesv2_wrapper.send_email( + from_address=self.sender_email, + to_addresses=self.recipient_emails, + subject="SESv2 Attachment Demo — Inline Image", + html_body=html_body, + text_body=( + "Inline Image Demo — Please view this email in an " + "HTML-capable client to see the embedded image." + ), + attachments=[attachment], + ) + + print(f" ✓ Email sent! MessageId: {message_id}") + print( + " The ContentId 'logo123' is referenced in the HTML body via\n" + " 'cid:logo123', which lets the image render inline.\n" + ) + + # ---------- Step 3: Bulk templated email with attachments ---------- + + def _step3_send_bulk_email_with_attachments(self) -> None: + """Sends bulk templated emails with attachments to multiple recipients.""" + print("\n--- Step 3: Send Bulk Templated Emails with Attachments ---\n") + print( + "Using SendBulkEmail to send a templated email with an attachment\n" + "to multiple recipients in a single API call. Each recipient gets\n" + "personalized content via template data.\n" + ) + + sample_content = b"This is a sample report attachment." + + attachment = { + "RawContent": sample_content, + "FileName": "sample-report.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Sample report for bulk recipients", + "ContentTransferEncoding": "BASE64", + } + + # Build one entry per recipient with personalized names. + names = ["Alice", "Bob", "Charlie", "Diana", "Eve"] + bulk_entries = [] + for i, email in enumerate(self.recipient_emails): + name = names[i] if i < len(names) else f"Recipient{i + 1}" + bulk_entries.append( + { + "Destination": {"ToAddresses": [email]}, + "ReplacementEmailContent": { + "ReplacementTemplate": { + "ReplacementTemplateData": json.dumps( + {"name": name} + ) + } + }, + } + ) + + results = self.sesv2_wrapper.send_bulk_email( + from_address=self.sender_email, + template_name=self.TEMPLATE_NAME, + default_template_data='{"name": "Valued Customer"}', + bulk_entries=bulk_entries, + attachments=[attachment], + ) + + print(" Bulk email results:") + for idx, result in enumerate(results): + status = result.get("Status", "Unknown") + msg_id = result.get("MessageId", "N/A") + error = result.get("Error", "") + recipient = ( + self.recipient_emails[idx] + if idx < len(self.recipient_emails) + else "Unknown" + ) + print(f" {recipient}: Status={status}, MessageId={msg_id}") + if error: + print(f" Error: {error}") + + print( + "\n All recipients receive the same attachment(s) defined in\n" + " DefaultContent. Template data is personalized per recipient.\n" + ) + + # ---------- Cleanup ---------- + + def _cleanup(self) -> None: + """Deletes the email template and optionally the email identity.""" + print("\n--- Cleanup ---\n") + + # Delete the email template. + try: + self.sesv2_wrapper.delete_email_template(self.TEMPLATE_NAME) + print(f" ✓ Template '{self.TEMPLATE_NAME}' deleted.") + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + print( + f" Template '{self.TEMPLATE_NAME}' was already deleted." + ) + else: + logger.error("Failed to delete template: %s", err) + + # Optionally delete the email identity. + if self.identity_was_created and self.sender_email: + delete_identity = q.ask( + f"Delete the email identity '{self.sender_email}'? (y/n) ", + q.is_yesno, + ) + if delete_identity: + try: + self.sesv2_wrapper.delete_email_identity( + self.sender_email + ) + print( + f" ✓ Email identity '{self.sender_email}' deleted." + ) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + print( + f" Identity '{self.sender_email}' was " + "already deleted." + ) + else: + logger.error( + "Failed to delete identity: %s", err + ) + else: + print( + f" Skipping identity deletion for {self.sender_email}." + ) + else: + print( + " Sender identity was pre-existing. Skipping deletion." + ) + + print("\nAll resources have been cleaned up.") + print("-" * 88) + + +# snippet-end:[python.example_code.sesv2.Scenario_EmailAttachments] + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + try: + scenario = SESv2EmailAttachmentsScenario(SESv2Wrapper.from_client()) + scenario.run_scenario() + except Exception: + logging.exception("Something went wrong with the scenario.") \ No newline at end of file diff --git a/python/example_code/sesv2/sesv2_hello.py b/python/example_code/sesv2/sesv2_hello.py new file mode 100644 index 00000000000..c707c92d3a9 --- /dev/null +++ b/python/example_code/sesv2/sesv2_hello.py @@ -0,0 +1,77 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Purpose + Shows how to get started with Amazon SESv2 by listing email identities + associated with the account. +""" + +import logging + +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.sesv2.Hello] +def hello_sesv2(sesv2_client): + """ + Use the AWS SDK for Python (Boto3) to create an Amazon SESv2 client and + list the email identities in your account. This example uses the default + settings specified in your shared credentials and config files. + + :param sesv2_client: A Boto3 SESv2 client object. + """ + print("Hello, Amazon SESv2! Let's list your email identities:\n") + + identity_count = 0 + next_token = None + try: + while True: + kwargs = {"PageSize": 20} + if next_token: + kwargs["NextToken"] = next_token + response = sesv2_client.list_email_identities(**kwargs) + identities = response.get("EmailIdentities", []) + for identity in identities: + identity_count += 1 + identity_name = identity.get("IdentityName", "Unknown") + identity_type = identity.get("IdentityType", "Unknown") + verification_status = identity.get( + "VerificationStatus", "Unknown" + ) + sending_enabled = identity.get("SendingEnabled", False) + print( + f" Identity: {identity_name}" + f" Type: {identity_type}" + f" Status: {verification_status}" + f" Sending: {'Enabled' if sending_enabled else 'Disabled'}" + ) + next_token = response.get("NextToken") + if not next_token: + break + + if identity_count == 0: + print( + "No email identities found. " + "Use CreateEmailIdentity to add one." + ) + else: + print(f"\nFound {identity_count} email identity(ies).") + + except ClientError as err: + logger.error( + "Couldn't list email identities. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + +# snippet-end:[python.example_code.sesv2.Hello] + + +if __name__ == "__main__": + hello_sesv2(boto3.client("sesv2")) diff --git a/python/example_code/sesv2/sesv2_wrapper.py b/python/example_code/sesv2/sesv2_wrapper.py new file mode 100644 index 00000000000..870314325b7 --- /dev/null +++ b/python/example_code/sesv2/sesv2_wrapper.py @@ -0,0 +1,352 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Encapsulates Amazon SESv2 actions for sending emails with attachments. +""" + +import logging +from typing import Any, Dict, List, Optional + +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.sesv2.SESv2Wrapper.class] +# snippet-start:[python.example_code.sesv2.SESv2Wrapper.decl] +class SESv2Wrapper: + """Encapsulates Amazon SESv2 email sending actions.""" + + def __init__(self, sesv2_client: Any) -> None: + """ + Initializes the SESv2Wrapper with an SESv2 client. + + :param sesv2_client: A Boto3 SESv2 client. + """ + self.sesv2_client = sesv2_client + + @classmethod + def from_client(cls) -> "SESv2Wrapper": + """ + Creates an SESv2Wrapper instance with a default Boto3 SESv2 client. + + :return: A new SESv2Wrapper instance. + """ + sesv2_client = boto3.client("sesv2") + return cls(sesv2_client) + + # snippet-end:[python.example_code.sesv2.SESv2Wrapper.decl] + + # snippet-start:[python.example_code.sesv2.GetEmailIdentity] + def get_email_identity(self, email_address: str) -> Dict[str, Any]: + """ + Gets information about an email identity, including its verification status. + + :param email_address: The email address or domain to look up. + :return: A dictionary with identity information including verification status. + :raises ClientError: If the identity is not found (NotFoundException). + """ + try: + response = self.sesv2_client.get_email_identity( + EmailIdentity=email_address + ) + logger.info("Got email identity for %s.", email_address) + return response + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + logger.info( + "Email identity %s not found.", email_address + ) + else: + logger.error( + "Couldn't get email identity %s. Here's why: %s: %s", + email_address, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.GetEmailIdentity] + + # snippet-start:[python.example_code.sesv2.CreateEmailIdentity] + def create_email_identity(self, email_address: str) -> Dict[str, Any]: + """ + Starts the process of verifying an email identity (email address or domain). + + :param email_address: The email address or domain to verify. + :return: A dictionary with the identity type and verification status. + :raises ClientError: If the limit is exceeded (LimitExceededException). + """ + try: + response = self.sesv2_client.create_email_identity( + EmailIdentity=email_address + ) + logger.info( + "Started verification for email identity %s.", email_address + ) + return response + except ClientError as err: + if err.response["Error"]["Code"] == "LimitExceededException": + logger.error( + "Couldn't create email identity %s. You have exceeded " + "the maximum number of email identities. " + "Use an existing verified identity.", + email_address, + ) + else: + logger.error( + "Couldn't create email identity %s. Here's why: %s: %s", + email_address, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.CreateEmailIdentity] + + # snippet-start:[python.example_code.sesv2.CreateEmailTemplate] + def create_email_template( + self, + template_name: str, + subject: str, + html_body: str, + text_body: str, + ) -> None: + """ + Creates an email template for use with templated and bulk email sends. + + :param template_name: The name for the new template. + :param subject: The subject line of the template. May include {{placeholders}}. + :param html_body: The HTML body of the template. + :param text_body: The plain text body of the template. + :raises ClientError: If the template limit is exceeded (LimitExceededException). + """ + try: + self.sesv2_client.create_email_template( + TemplateName=template_name, + TemplateContent={ + "Subject": subject, + "Html": html_body, + "Text": text_body, + }, + ) + logger.info("Created email template %s.", template_name) + except ClientError as err: + if err.response["Error"]["Code"] == "LimitExceededException": + logger.error( + "Couldn't create email template %s. You have exceeded " + "the maximum number of email templates. " + "Delete unused templates first.", + template_name, + ) + else: + logger.error( + "Couldn't create email template %s. Here's why: %s: %s", + template_name, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.CreateEmailTemplate] + + # snippet-start:[python.example_code.sesv2.SendEmail] + def send_email( + self, + from_address: str, + to_addresses: List[str], + subject: str, + html_body: str, + text_body: str, + attachments: Optional[List[Dict[str, Any]]] = None, + ) -> str: + """ + Sends a simple email message with optional attachments. + + SES handles MIME construction automatically when using attachments + with the Simple content type, so developers don't need to build + raw MIME messages. + + :param from_address: The verified sender email address. + :param to_addresses: A list of recipient email addresses. + :param subject: The subject line of the email. + :param html_body: The HTML body content. + :param text_body: The plain text body content. + :param attachments: An optional list of attachment dictionaries. Each + attachment should contain 'RawContent' (bytes), 'FileName' (str), + and optionally 'ContentType', 'ContentDisposition', 'ContentId', + 'ContentDescription', and 'ContentTransferEncoding'. + :return: The MessageId of the sent email. + :raises ClientError: If the message is rejected (MessageRejected). + """ + try: + simple_message: Dict[str, Any] = { + "Subject": {"Data": subject}, + "Body": { + "Html": {"Data": html_body}, + "Text": {"Data": text_body}, + }, + } + + if attachments: + simple_message["Attachments"] = attachments + + response = self.sesv2_client.send_email( + FromEmailAddress=from_address, + Destination={"ToAddresses": to_addresses}, + Content={"Simple": simple_message}, + ) + message_id = response["MessageId"] + logger.info( + "Sent email from %s to %s. MessageId: %s", + from_address, + to_addresses, + message_id, + ) + return message_id + except ClientError as err: + if err.response["Error"]["Code"] == "MessageRejected": + logger.error( + "Message was rejected. Check that attachments use " + "supported file types and total message size is " + "under 40 MB. Details: %s", + err.response["Error"]["Message"], + ) + else: + logger.error( + "Couldn't send email. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.SendEmail] + + # snippet-start:[python.example_code.sesv2.SendBulkEmail] + def send_bulk_email( + self, + from_address: str, + template_name: str, + default_template_data: str, + bulk_entries: List[Dict[str, Any]], + attachments: Optional[List[Dict[str, Any]]] = None, + ) -> List[Dict[str, Any]]: + """ + Sends a templated email to multiple recipients in a single API call. + + All recipients receive the same attachment(s) defined in the default + content, while template data can be personalized per recipient. + + :param from_address: The verified sender email address. + :param template_name: The name of an existing email template. + :param default_template_data: Default JSON template data string. + :param bulk_entries: A list of BulkEmailEntry dicts, each containing + 'Destination' and optionally 'ReplacementEmailContent'. + :param attachments: An optional list of attachment dicts for all + recipients. + :return: A list of BulkEmailEntryResult dicts with status and MessageId. + :raises ClientError: If the message is rejected (MessageRejected). + """ + try: + template_content: Dict[str, Any] = { + "TemplateName": template_name, + "TemplateData": default_template_data, + } + + if attachments: + template_content["Attachments"] = attachments + + response = self.sesv2_client.send_bulk_email( + FromEmailAddress=from_address, + DefaultContent={"Template": template_content}, + BulkEmailEntries=bulk_entries, + ) + results = response.get("BulkEmailEntryResults", []) + logger.info( + "Sent bulk email from %s to %d recipients.", + from_address, + len(bulk_entries), + ) + return results + except ClientError as err: + if err.response["Error"]["Code"] == "MessageRejected": + logger.error( + "Bulk message was rejected. Check that the template " + "exists, attachment file types are supported, and " + "total message size is within limits. Details: %s", + err.response["Error"]["Message"], + ) + else: + logger.error( + "Couldn't send bulk email. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.SendBulkEmail] + + # snippet-start:[python.example_code.sesv2.DeleteEmailTemplate] + def delete_email_template(self, template_name: str) -> None: + """ + Deletes an email template. + + :param template_name: The name of the template to delete. + :raises ClientError: If the template is not found (NotFoundException). + """ + try: + self.sesv2_client.delete_email_template( + TemplateName=template_name + ) + logger.info("Deleted email template %s.", template_name) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + logger.info( + "Email template %s not found or already deleted.", + template_name, + ) + else: + logger.error( + "Couldn't delete email template %s. Here's why: %s: %s", + template_name, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.DeleteEmailTemplate] + + # snippet-start:[python.example_code.sesv2.DeleteEmailIdentity] + def delete_email_identity(self, email_address: str) -> None: + """ + Deletes an email identity. + + :param email_address: The email address or domain to delete. + :raises ClientError: If the identity is not found (NotFoundException). + """ + try: + self.sesv2_client.delete_email_identity( + EmailIdentity=email_address + ) + logger.info("Deleted email identity %s.", email_address) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + logger.info( + "Email identity %s not found or already deleted.", + email_address, + ) + else: + logger.error( + "Couldn't delete email identity %s. Here's why: %s: %s", + email_address, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.DeleteEmailIdentity] + + +# snippet-end:[python.example_code.sesv2.SESv2Wrapper.class] \ No newline at end of file diff --git a/python/example_code/sesv2/test_sesv2_email_attachments.py b/python/example_code/sesv2/test_sesv2_email_attachments.py new file mode 100644 index 00000000000..71b638d68d1 --- /dev/null +++ b/python/example_code/sesv2/test_sesv2_email_attachments.py @@ -0,0 +1,210 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Integration tests for the SESv2 Email Attachments scenario. + +These tests call real AWS SESv2 APIs and require a verified email identity. +Set the environment variable SENDER_EMAIL to a verified SES email address. +""" + +import os + +import boto3 +import pytest + +from sesv2_wrapper import SESv2Wrapper +from scenario_sesv2_email_attachments import SESv2EmailAttachmentsScenario + + +@pytest.mark.integ +def test_sesv2_wrapper_get_email_identity(): + """Test that get_email_identity returns identity info for a verified sender.""" + sender_email = os.environ.get("SENDER_EMAIL") + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + try: + result = wrapper.get_email_identity(sender_email) + assert "VerifiedForSendingStatus" in result + finally: + # No cleanup needed for read-only operation. + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_create_and_delete_template(): + """Test creating and deleting an email template.""" + wrapper = SESv2Wrapper.from_client() + template_name = "IntegTestTemplate" + try: + wrapper.create_email_template( + template_name=template_name, + subject="Test Subject for {{name}}", + html_body="

Hello {{name}}

", + text_body="Hello {{name}}", + ) + finally: + try: + wrapper.delete_email_template(template_name) + except Exception: + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_send_email_with_attachment(): + """Test sending a simple email with a file attachment.""" + sender_email = os.environ.get("SENDER_EMAIL") + recipient_email = os.environ.get("RECIPIENT_EMAIL", sender_email) + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + sample_content = b"Integration test attachment content." + + attachment = { + "RawContent": sample_content, + "FileName": "integ-test.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Integration test file", + "ContentTransferEncoding": "BASE64", + } + + try: + message_id = wrapper.send_email( + from_address=sender_email, + to_addresses=[recipient_email], + subject="SESv2 Integration Test — Attachment", + html_body="

Integration test with attachment.

", + text_body="Integration test with attachment.", + attachments=[attachment], + ) + assert message_id is not None + assert len(message_id) > 0 + finally: + # No cleanup needed for sent emails. + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_send_email_with_inline_image(): + """Test sending a simple email with an inline image.""" + sender_email = os.environ.get("SENDER_EMAIL") + recipient_email = os.environ.get("RECIPIENT_EMAIL", sender_email) + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + + # Minimal 1x1 PNG + sample_image = ( + b"\x89PNG\r\n\x1a\n" + b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01" + b"\x08\x02\x00\x00\x00\x90wS\xde" + b"\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01" + b"\x00\x05\x18\xd8N" + b"\x00\x00\x00\x00IEND\xaeB`\x82" + ) + + attachment = { + "RawContent": sample_image, + "FileName": "test-logo.png", + "ContentType": "image/png", + "ContentDisposition": "INLINE", + "ContentId": "testlogo123", + "ContentDescription": "Test logo", + "ContentTransferEncoding": "BASE64", + } + + html_body = ( + '

Test

' + 'Test Logo' + '' + ) + + try: + message_id = wrapper.send_email( + from_address=sender_email, + to_addresses=[recipient_email], + subject="SESv2 Integration Test — Inline Image", + html_body=html_body, + text_body="Inline image test — view in HTML client.", + attachments=[attachment], + ) + assert message_id is not None + assert len(message_id) > 0 + finally: + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_send_bulk_email_with_attachment(): + """Test sending bulk templated email with an attachment.""" + sender_email = os.environ.get("SENDER_EMAIL") + recipient_email = os.environ.get("RECIPIENT_EMAIL", sender_email) + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + template_name = "IntegTestBulkTemplate" + + sample_content = b"Bulk test attachment content." + attachment = { + "RawContent": sample_content, + "FileName": "bulk-test.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Bulk test file", + "ContentTransferEncoding": "BASE64", + } + + try: + wrapper.create_email_template( + template_name=template_name, + subject="Bulk Test for {{name}}", + html_body="

Hello {{name}}

Attached document.

", + text_body="Hello {{name}}, attached document.", + ) + + bulk_entries = [ + { + "Destination": {"ToAddresses": [recipient_email]}, + "ReplacementEmailContent": { + "ReplacementTemplate": { + "ReplacementTemplateData": '{"name": "TestUser"}' + } + }, + } + ] + + results = wrapper.send_bulk_email( + from_address=sender_email, + template_name=template_name, + default_template_data='{"name": "Default"}', + bulk_entries=bulk_entries, + attachments=[attachment], + ) + + assert results is not None + assert len(results) == 1 + assert results[0].get("Status") == "SUCCESS" + finally: + try: + wrapper.delete_email_template(template_name) + except Exception: + pass + + +@pytest.mark.integ +def test_sesv2_hello(capsys): + """Test the Hello SESv2 example.""" + from sesv2_hello import hello_sesv2 + + try: + hello_sesv2(boto3.client("sesv2")) + captured = capsys.readouterr() + assert "Hello, Amazon SESv2!" in captured.out + finally: + pass \ No newline at end of file From 8fd44dc12cbbd4bd8b899ba9ffe3739281befae2 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Tue, 14 Apr 2026 14:39:03 -0500 Subject: [PATCH 03/25] Updates to README and tools requirements. --- .tools/base_requirements.txt | 4 +- .tools/readmes/requirements_freeze.txt | 2 +- python/example_code/sesv2/README.md | 51 ++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/.tools/base_requirements.txt b/.tools/base_requirements.txt index 07e44a90fcf..9a251cb9671 100644 --- a/.tools/base_requirements.txt +++ b/.tools/base_requirements.txt @@ -1,7 +1,7 @@ -black==26.3.1 +black==24.10.0 flake8==6.1.0 mypy-extensions==1.0.0 -pathspec==1.0.0 +pathspec==0.11.2 PyYAML==6.0.1 requests==2.33.0 typer==0.15.2 diff --git a/.tools/readmes/requirements_freeze.txt b/.tools/readmes/requirements_freeze.txt index dcb659f4610..90f55ef6e05 100644 --- a/.tools/readmes/requirements_freeze.txt +++ b/.tools/readmes/requirements_freeze.txt @@ -1,5 +1,5 @@ aws_doc_sdk_examples_tools @ git+https://github.com/awsdocs/aws-doc-sdk-examples-tools@2025.41.0 -black==26.3.1 +black==24.10.0 certifi==2025.1.31 charset-normalizer==3.4.1 click==8.1.8 diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index a35582ddd21..e03ec0a6da0 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -34,17 +34,23 @@ python -m pip install -r requirements.txt +### Get started + +- [Hello Amazon SES v2 API](sesv2_hello.py#L18) (`ListEmailIdentities`) + + ### Single actions Code excerpts that show you how to call individual service functions. - [CreateContact](newsletter.py#L155) - [CreateContactList](newsletter.py#L105) -- [CreateEmailIdentity](newsletter.py#L92) -- [CreateEmailTemplate](newsletter.py#L118) +- [CreateEmailIdentity](sesv2_wrapper.py#L73) +- [CreateEmailTemplate](sesv2_wrapper.py#L109) - [DeleteContactList](newsletter.py#L258) -- [DeleteEmailIdentity](newsletter.py#L286) -- [DeleteEmailTemplate](newsletter.py#L271) +- [DeleteEmailIdentity](sesv2_wrapper.py#L321) +- [DeleteEmailTemplate](sesv2_wrapper.py#L291) +- [GetEmailIdentity](sesv2_wrapper.py#L42) - [ListContacts](newsletter.py#L198) - [SendEmail](newsletter.py#L164) @@ -53,8 +59,14 @@ Code excerpts that show you how to call individual service functions. Code examples that show you how to accomplish a specific task by calling multiple functions within the same service. +- [Email Attachments Scenario](sesv2_wrapper.py) - [Newsletter scenario](newsletter.py) +### Actions +_Actions_ are code excerpts from larger programs and must be run in context. While actions show you how to call individual service functions, you can see actions in context in their related scenarios. + +- [SendBulkEmail](sesv2_wrapper.py#L227) + @@ -74,7 +86,38 @@ To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer +#### Hello Amazon SES v2 API + +This example shows you how to get started using Amazon SES v2 API. + +``` +python sesv2_hello.py +``` + + +#### Email Attachments Scenario + +This example shows you how to send emails with attachments using Amazon SES v2 API. + +- Verify sender email identity. +- Create an email template for bulk sends. +- Send a simple email with a file attachment. +- Send a simple email with an inline image. +- Send bulk templated emails with attachments. +- Clean up resources. + + + + +Start the example by running the following at a command prompt: + +``` +python sesv2_wrapper.py +``` + + + #### Newsletter scenario From 689e773d1520b4556358391ffd5e2bbecb74db98 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Thu, 9 Apr 2026 10:36:16 -0500 Subject: [PATCH 04/25] Initial commit of codeloom code --- .doc_gen/metadata/sesv2_metadata.yaml | 89 ++++ .gitignore | 1 + python/example_code/sesv2/requirements.txt | 3 +- .../sesv2/scenario_sesv2_email_attachments.py | 396 ++++++++++++++++++ python/example_code/sesv2/sesv2_hello.py | 77 ++++ python/example_code/sesv2/sesv2_wrapper.py | 352 ++++++++++++++++ .../sesv2/test_sesv2_email_attachments.py | 210 ++++++++++ 7 files changed, 1127 insertions(+), 1 deletion(-) create mode 100644 python/example_code/sesv2/scenario_sesv2_email_attachments.py create mode 100644 python/example_code/sesv2/sesv2_hello.py create mode 100644 python/example_code/sesv2/sesv2_wrapper.py create mode 100644 python/example_code/sesv2/test_sesv2_email_attachments.py diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 313a51113c9..45b53bca279 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -101,6 +101,15 @@ sesv2_CreateContact: sesv2: {CreateContact} sesv2_GetEmailIdentity: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.GetEmailIdentity Rust: versions: - sdk_version: 1 @@ -248,6 +257,10 @@ sesv2_SendEmail: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.template + - description: Sends a message with optional attachments. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SendEmail services: sesv2: {SendEmail} sesv2_CreateEmailIdentity: @@ -290,6 +303,10 @@ sesv2_CreateEmailIdentity: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailIdentity + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.CreateEmailIdentity Rust: versions: - sdk_version: 1 @@ -341,6 +358,10 @@ sesv2_CreateEmailTemplate: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailTemplate + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.CreateEmailTemplate Rust: versions: - sdk_version: 1 @@ -443,6 +464,10 @@ sesv2_DeleteEmailIdentity: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailIdentity + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.DeleteEmailIdentity Rust: versions: - sdk_version: 1 @@ -494,6 +519,10 @@ sesv2_DeleteEmailTemplate: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailTemplate + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.DeleteEmailTemplate Rust: versions: - sdk_version: 1 @@ -581,3 +610,63 @@ sesv2_NewsletterWorkflow: services: sesv2: {CreateContactList, CreateContact, ListContacts, SendEmail.simple, SendEmail.template, CreateEmailIdentity, CreateEmailTemplate, DeleteContactList, DeleteEmailIdentity, DeleteEmailTemplate} + +sesv2_Hello: + title: Hello &SESv2; + title_abbrev: Hello &SESv2; + synopsis: get started using &SESv2;. + category: Hello + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.Hello + services: + sesv2: {ListEmailIdentities} +sesv2_SendBulkEmail: + title: Send bulk email with &SESv2; + title_abbrev: Send bulk email + synopsis: send bulk email with &SESv2;. + category: Actions + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SendBulkEmail + services: + sesv2: {SendBulkEmail} +sesv2_Scenario_EmailAttachments: + title: Send emails with attachments using &SESv2; + title_abbrev: Email Attachments Scenario + synopsis: send emails with attachments using &SESv2;. + synopsis_list: + - Verify sender email identity. + - Create an email template for bulk sends. + - Send a simple email with a file attachment. + - Send a simple email with an inline image. + - Send bulk templated emails with attachments. + - Clean up resources. + category: Scenarios + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: Create an SESv2 wrapper class to manage operations. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.class + - description: Run an interactive scenario demonstrating email attachments. + snippet_tags: + - python.example_code.sesv2.Scenario_EmailAttachments + services: + sesv2: {SendEmail, SendBulkEmail, CreateEmailIdentity, CreateEmailTemplate, GetEmailIdentity, DeleteEmailTemplate, DeleteEmailIdentity} diff --git a/.gitignore b/.gitignore index 657ad41f507..f9318473e2b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ kotlin/services/**/.kotlin/ .kiro/settings/ .kiro/steering/ +/codeloom_outputs diff --git a/python/example_code/sesv2/requirements.txt b/python/example_code/sesv2/requirements.txt index 621e276912d..1ce66d6ec91 100644 --- a/python/example_code/sesv2/requirements.txt +++ b/python/example_code/sesv2/requirements.txt @@ -1,2 +1,3 @@ -boto3>=1.26.79 +boto3>=1.35.0 +botocore>=1.35.0 pytest>=7.2.1 diff --git a/python/example_code/sesv2/scenario_sesv2_email_attachments.py b/python/example_code/sesv2/scenario_sesv2_email_attachments.py new file mode 100644 index 00000000000..198d35275e2 --- /dev/null +++ b/python/example_code/sesv2/scenario_sesv2_email_attachments.py @@ -0,0 +1,396 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Purpose + Shows how to use the AWS SDK for Python (Boto3) with Amazon SESv2 to + send emails with attachments. This scenario demonstrates three use cases: + 1. Send a simple email with a file attachment. + 2. Send a simple email with an inline image rendered in the HTML body. + 3. Send bulk templated emails with attachments to multiple recipients. + + The new attachment support eliminates the need for developers to construct + raw MIME messages — SES handles the MIME assembly automatically. +""" + +import json +import logging +import sys + +import boto3 +from botocore.exceptions import ClientError + +from sesv2_wrapper import SESv2Wrapper + +# Add relative path to include demo_tools in this code example without need for setup. +sys.path.append("../..") +import demo_tools.question as q # noqa + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.sesv2.Scenario_EmailAttachments] +class SESv2EmailAttachmentsScenario: + """ + Demonstrates sending emails with attachments using Amazon SESv2. + + This scenario walks through: + 1. Setting up an email identity and template. + 2. Sending a simple email with a file attachment. + 3. Sending a simple email with an inline image. + 4. Sending bulk templated emails with attachments. + 5. Cleaning up created resources. + """ + + TEMPLATE_NAME = "AttachmentDemoTemplate" + + def __init__(self, sesv2_wrapper: SESv2Wrapper) -> None: + """ + :param sesv2_wrapper: An instance of the SESv2Wrapper class. + """ + self.sesv2_wrapper = sesv2_wrapper + self.sender_email = "" + self.recipient_emails: list = [] + self.identity_was_created = False + + def run_scenario(self) -> None: + """Runs the SESv2 email attachments scenario.""" + print("-" * 88) + print("Welcome to the Amazon SESv2 Email Attachments Scenario!") + print("-" * 88) + print( + "This scenario demonstrates how to send emails with attachments\n" + "using the new SESv2 attachment support. SES handles MIME\n" + "construction automatically, so you don't need to build raw\n" + "MIME messages.\n" + ) + + try: + self._setup() + self._step1_send_email_with_attachment() + self._step2_send_email_with_inline_image() + self._step3_send_bulk_email_with_attachments() + except Exception as e: + logger.error("Scenario failed: %s", e) + print(f"\nThe scenario encountered an error: {e}") + finally: + self._cleanup() + + # ---------- Setup ---------- + + def _setup(self) -> None: + """ + Prompts for configuration, verifies the sender identity, prepares a + sample attachment, and creates an email template. + """ + print("\n--- Setup ---\n") + + # Prompt for sender and recipient addresses. + print( + "Both sender and recipient addresses must be verified if your\n" + "account is in the SES sandbox.\n" + ) + self.sender_email = q.ask( + "Enter a verified sender email address: " + ) + recipient_input = q.ask( + "Enter one or more recipient email addresses (comma-separated): " + ) + self.recipient_emails = [ + addr.strip() for addr in recipient_input.split(",") if addr.strip() + ] + + # Verify the sender identity. + print(f"\nChecking identity for {self.sender_email}...") + try: + identity_info = self.sesv2_wrapper.get_email_identity( + self.sender_email + ) + verified = identity_info.get("VerifiedForSendingStatus", False) + if verified: + print(f" ✓ {self.sender_email} is verified and ready to send.") + else: + print( + f" ⚠ {self.sender_email} exists but is not yet verified." + ) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + print( + f" Identity {self.sender_email} not found. " + "Creating it now..." + ) + result = self.sesv2_wrapper.create_email_identity( + self.sender_email + ) + self.identity_was_created = True + print( + f" Identity created. Verification status: " + f"{result.get('VerifiedForSendingStatus', False)}" + ) + print( + " Check your inbox and click the verification link " + "before continuing." + ) + q.ask("Press Enter when you have verified the address...") + else: + raise + + # Create the email template for the bulk-send step. + print("\nCreating email template for the bulk email step...") + try: + self.sesv2_wrapper.create_email_template( + template_name=self.TEMPLATE_NAME, + subject="Bulk Email with Attachment for {{name}}", + html_body=( + "

Hello {{name}}

" + "

Please find the attached document.

" + ), + text_body=( + "Hello {{name}}, Please find the attached document." + ), + ) + print(f" ✓ Template '{self.TEMPLATE_NAME}' created.\n") + except ClientError as err: + if err.response["Error"]["Code"] == "AlreadyExistsException": + print( + f" Template '{self.TEMPLATE_NAME}' already exists. " + "Using it.\n" + ) + else: + raise + + # ---------- Step 1: Simple email with file attachment ---------- + + def _step1_send_email_with_attachment(self) -> None: + """Sends a simple email with a text file attachment.""" + print("\n--- Step 1: Send a Simple Email with a File Attachment ---\n") + print( + "Creating a sample text file attachment and sending it with\n" + "the Simple email content type. SES constructs the MIME message\n" + "automatically.\n" + ) + + # Prepare a sample text file as bytes. + sample_content = b"This is a sample report attachment." + + attachment = { + "RawContent": sample_content, + "FileName": "sample-report.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Sample report text file", + "ContentTransferEncoding": "BASE64", + } + + print( + "Note: When using an AWS SDK, the SDK handles base64 encoding\n" + "automatically. Direct API callers must encode content themselves.\n" + ) + + message_id = self.sesv2_wrapper.send_email( + from_address=self.sender_email, + to_addresses=self.recipient_emails, + subject="SESv2 Attachment Demo — Simple Email with Attachment", + html_body=( + "

Attachment Demo

" + "

Please see the attached report document.

" + ), + text_body="Please see the attached report document.", + attachments=[attachment], + ) + + print(f" ✓ Email sent! MessageId: {message_id}") + print( + " SES automatically constructed the MIME message with the " + "attachment.\n" + ) + + # ---------- Step 2: Simple email with inline image ---------- + + def _step2_send_email_with_inline_image(self) -> None: + """Sends a simple email with an inline image that renders in HTML.""" + print("\n--- Step 2: Send a Simple Email with an Inline Image ---\n") + print( + "This step demonstrates INLINE disposition. The image renders\n" + "directly in the HTML body using a 'cid:' reference instead of\n" + "appearing as a downloadable attachment.\n" + ) + + # Create a minimal 1x1 red PNG (valid PNG file). + sample_image = ( + b"\x89PNG\r\n\x1a\n" # PNG signature + b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01" + b"\x08\x02\x00\x00\x00\x90wS\xde" # 1x1 RGB + b"\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01" + b"\x00\x05\x18\xd8N" # compressed data + b"\x00\x00\x00\x00IEND\xaeB`\x82" # IEND + ) + + attachment = { + "RawContent": sample_image, + "FileName": "logo.png", + "ContentType": "image/png", + "ContentDisposition": "INLINE", + "ContentId": "logo123", + "ContentDescription": "Company logo", + "ContentTransferEncoding": "BASE64", + } + + html_body = ( + "" + "

Inline Image Demo

" + "

Here is our logo:

" + 'Company Logo' + "" + ) + + message_id = self.sesv2_wrapper.send_email( + from_address=self.sender_email, + to_addresses=self.recipient_emails, + subject="SESv2 Attachment Demo — Inline Image", + html_body=html_body, + text_body=( + "Inline Image Demo — Please view this email in an " + "HTML-capable client to see the embedded image." + ), + attachments=[attachment], + ) + + print(f" ✓ Email sent! MessageId: {message_id}") + print( + " The ContentId 'logo123' is referenced in the HTML body via\n" + " 'cid:logo123', which lets the image render inline.\n" + ) + + # ---------- Step 3: Bulk templated email with attachments ---------- + + def _step3_send_bulk_email_with_attachments(self) -> None: + """Sends bulk templated emails with attachments to multiple recipients.""" + print("\n--- Step 3: Send Bulk Templated Emails with Attachments ---\n") + print( + "Using SendBulkEmail to send a templated email with an attachment\n" + "to multiple recipients in a single API call. Each recipient gets\n" + "personalized content via template data.\n" + ) + + sample_content = b"This is a sample report attachment." + + attachment = { + "RawContent": sample_content, + "FileName": "sample-report.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Sample report for bulk recipients", + "ContentTransferEncoding": "BASE64", + } + + # Build one entry per recipient with personalized names. + names = ["Alice", "Bob", "Charlie", "Diana", "Eve"] + bulk_entries = [] + for i, email in enumerate(self.recipient_emails): + name = names[i] if i < len(names) else f"Recipient{i + 1}" + bulk_entries.append( + { + "Destination": {"ToAddresses": [email]}, + "ReplacementEmailContent": { + "ReplacementTemplate": { + "ReplacementTemplateData": json.dumps( + {"name": name} + ) + } + }, + } + ) + + results = self.sesv2_wrapper.send_bulk_email( + from_address=self.sender_email, + template_name=self.TEMPLATE_NAME, + default_template_data='{"name": "Valued Customer"}', + bulk_entries=bulk_entries, + attachments=[attachment], + ) + + print(" Bulk email results:") + for idx, result in enumerate(results): + status = result.get("Status", "Unknown") + msg_id = result.get("MessageId", "N/A") + error = result.get("Error", "") + recipient = ( + self.recipient_emails[idx] + if idx < len(self.recipient_emails) + else "Unknown" + ) + print(f" {recipient}: Status={status}, MessageId={msg_id}") + if error: + print(f" Error: {error}") + + print( + "\n All recipients receive the same attachment(s) defined in\n" + " DefaultContent. Template data is personalized per recipient.\n" + ) + + # ---------- Cleanup ---------- + + def _cleanup(self) -> None: + """Deletes the email template and optionally the email identity.""" + print("\n--- Cleanup ---\n") + + # Delete the email template. + try: + self.sesv2_wrapper.delete_email_template(self.TEMPLATE_NAME) + print(f" ✓ Template '{self.TEMPLATE_NAME}' deleted.") + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + print( + f" Template '{self.TEMPLATE_NAME}' was already deleted." + ) + else: + logger.error("Failed to delete template: %s", err) + + # Optionally delete the email identity. + if self.identity_was_created and self.sender_email: + delete_identity = q.ask( + f"Delete the email identity '{self.sender_email}'? (y/n) ", + q.is_yesno, + ) + if delete_identity: + try: + self.sesv2_wrapper.delete_email_identity( + self.sender_email + ) + print( + f" ✓ Email identity '{self.sender_email}' deleted." + ) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + print( + f" Identity '{self.sender_email}' was " + "already deleted." + ) + else: + logger.error( + "Failed to delete identity: %s", err + ) + else: + print( + f" Skipping identity deletion for {self.sender_email}." + ) + else: + print( + " Sender identity was pre-existing. Skipping deletion." + ) + + print("\nAll resources have been cleaned up.") + print("-" * 88) + + +# snippet-end:[python.example_code.sesv2.Scenario_EmailAttachments] + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + try: + scenario = SESv2EmailAttachmentsScenario(SESv2Wrapper.from_client()) + scenario.run_scenario() + except Exception: + logging.exception("Something went wrong with the scenario.") \ No newline at end of file diff --git a/python/example_code/sesv2/sesv2_hello.py b/python/example_code/sesv2/sesv2_hello.py new file mode 100644 index 00000000000..c707c92d3a9 --- /dev/null +++ b/python/example_code/sesv2/sesv2_hello.py @@ -0,0 +1,77 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Purpose + Shows how to get started with Amazon SESv2 by listing email identities + associated with the account. +""" + +import logging + +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.sesv2.Hello] +def hello_sesv2(sesv2_client): + """ + Use the AWS SDK for Python (Boto3) to create an Amazon SESv2 client and + list the email identities in your account. This example uses the default + settings specified in your shared credentials and config files. + + :param sesv2_client: A Boto3 SESv2 client object. + """ + print("Hello, Amazon SESv2! Let's list your email identities:\n") + + identity_count = 0 + next_token = None + try: + while True: + kwargs = {"PageSize": 20} + if next_token: + kwargs["NextToken"] = next_token + response = sesv2_client.list_email_identities(**kwargs) + identities = response.get("EmailIdentities", []) + for identity in identities: + identity_count += 1 + identity_name = identity.get("IdentityName", "Unknown") + identity_type = identity.get("IdentityType", "Unknown") + verification_status = identity.get( + "VerificationStatus", "Unknown" + ) + sending_enabled = identity.get("SendingEnabled", False) + print( + f" Identity: {identity_name}" + f" Type: {identity_type}" + f" Status: {verification_status}" + f" Sending: {'Enabled' if sending_enabled else 'Disabled'}" + ) + next_token = response.get("NextToken") + if not next_token: + break + + if identity_count == 0: + print( + "No email identities found. " + "Use CreateEmailIdentity to add one." + ) + else: + print(f"\nFound {identity_count} email identity(ies).") + + except ClientError as err: + logger.error( + "Couldn't list email identities. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + +# snippet-end:[python.example_code.sesv2.Hello] + + +if __name__ == "__main__": + hello_sesv2(boto3.client("sesv2")) diff --git a/python/example_code/sesv2/sesv2_wrapper.py b/python/example_code/sesv2/sesv2_wrapper.py new file mode 100644 index 00000000000..870314325b7 --- /dev/null +++ b/python/example_code/sesv2/sesv2_wrapper.py @@ -0,0 +1,352 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Encapsulates Amazon SESv2 actions for sending emails with attachments. +""" + +import logging +from typing import Any, Dict, List, Optional + +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.sesv2.SESv2Wrapper.class] +# snippet-start:[python.example_code.sesv2.SESv2Wrapper.decl] +class SESv2Wrapper: + """Encapsulates Amazon SESv2 email sending actions.""" + + def __init__(self, sesv2_client: Any) -> None: + """ + Initializes the SESv2Wrapper with an SESv2 client. + + :param sesv2_client: A Boto3 SESv2 client. + """ + self.sesv2_client = sesv2_client + + @classmethod + def from_client(cls) -> "SESv2Wrapper": + """ + Creates an SESv2Wrapper instance with a default Boto3 SESv2 client. + + :return: A new SESv2Wrapper instance. + """ + sesv2_client = boto3.client("sesv2") + return cls(sesv2_client) + + # snippet-end:[python.example_code.sesv2.SESv2Wrapper.decl] + + # snippet-start:[python.example_code.sesv2.GetEmailIdentity] + def get_email_identity(self, email_address: str) -> Dict[str, Any]: + """ + Gets information about an email identity, including its verification status. + + :param email_address: The email address or domain to look up. + :return: A dictionary with identity information including verification status. + :raises ClientError: If the identity is not found (NotFoundException). + """ + try: + response = self.sesv2_client.get_email_identity( + EmailIdentity=email_address + ) + logger.info("Got email identity for %s.", email_address) + return response + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + logger.info( + "Email identity %s not found.", email_address + ) + else: + logger.error( + "Couldn't get email identity %s. Here's why: %s: %s", + email_address, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.GetEmailIdentity] + + # snippet-start:[python.example_code.sesv2.CreateEmailIdentity] + def create_email_identity(self, email_address: str) -> Dict[str, Any]: + """ + Starts the process of verifying an email identity (email address or domain). + + :param email_address: The email address or domain to verify. + :return: A dictionary with the identity type and verification status. + :raises ClientError: If the limit is exceeded (LimitExceededException). + """ + try: + response = self.sesv2_client.create_email_identity( + EmailIdentity=email_address + ) + logger.info( + "Started verification for email identity %s.", email_address + ) + return response + except ClientError as err: + if err.response["Error"]["Code"] == "LimitExceededException": + logger.error( + "Couldn't create email identity %s. You have exceeded " + "the maximum number of email identities. " + "Use an existing verified identity.", + email_address, + ) + else: + logger.error( + "Couldn't create email identity %s. Here's why: %s: %s", + email_address, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.CreateEmailIdentity] + + # snippet-start:[python.example_code.sesv2.CreateEmailTemplate] + def create_email_template( + self, + template_name: str, + subject: str, + html_body: str, + text_body: str, + ) -> None: + """ + Creates an email template for use with templated and bulk email sends. + + :param template_name: The name for the new template. + :param subject: The subject line of the template. May include {{placeholders}}. + :param html_body: The HTML body of the template. + :param text_body: The plain text body of the template. + :raises ClientError: If the template limit is exceeded (LimitExceededException). + """ + try: + self.sesv2_client.create_email_template( + TemplateName=template_name, + TemplateContent={ + "Subject": subject, + "Html": html_body, + "Text": text_body, + }, + ) + logger.info("Created email template %s.", template_name) + except ClientError as err: + if err.response["Error"]["Code"] == "LimitExceededException": + logger.error( + "Couldn't create email template %s. You have exceeded " + "the maximum number of email templates. " + "Delete unused templates first.", + template_name, + ) + else: + logger.error( + "Couldn't create email template %s. Here's why: %s: %s", + template_name, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.CreateEmailTemplate] + + # snippet-start:[python.example_code.sesv2.SendEmail] + def send_email( + self, + from_address: str, + to_addresses: List[str], + subject: str, + html_body: str, + text_body: str, + attachments: Optional[List[Dict[str, Any]]] = None, + ) -> str: + """ + Sends a simple email message with optional attachments. + + SES handles MIME construction automatically when using attachments + with the Simple content type, so developers don't need to build + raw MIME messages. + + :param from_address: The verified sender email address. + :param to_addresses: A list of recipient email addresses. + :param subject: The subject line of the email. + :param html_body: The HTML body content. + :param text_body: The plain text body content. + :param attachments: An optional list of attachment dictionaries. Each + attachment should contain 'RawContent' (bytes), 'FileName' (str), + and optionally 'ContentType', 'ContentDisposition', 'ContentId', + 'ContentDescription', and 'ContentTransferEncoding'. + :return: The MessageId of the sent email. + :raises ClientError: If the message is rejected (MessageRejected). + """ + try: + simple_message: Dict[str, Any] = { + "Subject": {"Data": subject}, + "Body": { + "Html": {"Data": html_body}, + "Text": {"Data": text_body}, + }, + } + + if attachments: + simple_message["Attachments"] = attachments + + response = self.sesv2_client.send_email( + FromEmailAddress=from_address, + Destination={"ToAddresses": to_addresses}, + Content={"Simple": simple_message}, + ) + message_id = response["MessageId"] + logger.info( + "Sent email from %s to %s. MessageId: %s", + from_address, + to_addresses, + message_id, + ) + return message_id + except ClientError as err: + if err.response["Error"]["Code"] == "MessageRejected": + logger.error( + "Message was rejected. Check that attachments use " + "supported file types and total message size is " + "under 40 MB. Details: %s", + err.response["Error"]["Message"], + ) + else: + logger.error( + "Couldn't send email. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.SendEmail] + + # snippet-start:[python.example_code.sesv2.SendBulkEmail] + def send_bulk_email( + self, + from_address: str, + template_name: str, + default_template_data: str, + bulk_entries: List[Dict[str, Any]], + attachments: Optional[List[Dict[str, Any]]] = None, + ) -> List[Dict[str, Any]]: + """ + Sends a templated email to multiple recipients in a single API call. + + All recipients receive the same attachment(s) defined in the default + content, while template data can be personalized per recipient. + + :param from_address: The verified sender email address. + :param template_name: The name of an existing email template. + :param default_template_data: Default JSON template data string. + :param bulk_entries: A list of BulkEmailEntry dicts, each containing + 'Destination' and optionally 'ReplacementEmailContent'. + :param attachments: An optional list of attachment dicts for all + recipients. + :return: A list of BulkEmailEntryResult dicts with status and MessageId. + :raises ClientError: If the message is rejected (MessageRejected). + """ + try: + template_content: Dict[str, Any] = { + "TemplateName": template_name, + "TemplateData": default_template_data, + } + + if attachments: + template_content["Attachments"] = attachments + + response = self.sesv2_client.send_bulk_email( + FromEmailAddress=from_address, + DefaultContent={"Template": template_content}, + BulkEmailEntries=bulk_entries, + ) + results = response.get("BulkEmailEntryResults", []) + logger.info( + "Sent bulk email from %s to %d recipients.", + from_address, + len(bulk_entries), + ) + return results + except ClientError as err: + if err.response["Error"]["Code"] == "MessageRejected": + logger.error( + "Bulk message was rejected. Check that the template " + "exists, attachment file types are supported, and " + "total message size is within limits. Details: %s", + err.response["Error"]["Message"], + ) + else: + logger.error( + "Couldn't send bulk email. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.SendBulkEmail] + + # snippet-start:[python.example_code.sesv2.DeleteEmailTemplate] + def delete_email_template(self, template_name: str) -> None: + """ + Deletes an email template. + + :param template_name: The name of the template to delete. + :raises ClientError: If the template is not found (NotFoundException). + """ + try: + self.sesv2_client.delete_email_template( + TemplateName=template_name + ) + logger.info("Deleted email template %s.", template_name) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + logger.info( + "Email template %s not found or already deleted.", + template_name, + ) + else: + logger.error( + "Couldn't delete email template %s. Here's why: %s: %s", + template_name, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.DeleteEmailTemplate] + + # snippet-start:[python.example_code.sesv2.DeleteEmailIdentity] + def delete_email_identity(self, email_address: str) -> None: + """ + Deletes an email identity. + + :param email_address: The email address or domain to delete. + :raises ClientError: If the identity is not found (NotFoundException). + """ + try: + self.sesv2_client.delete_email_identity( + EmailIdentity=email_address + ) + logger.info("Deleted email identity %s.", email_address) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + logger.info( + "Email identity %s not found or already deleted.", + email_address, + ) + else: + logger.error( + "Couldn't delete email identity %s. Here's why: %s: %s", + email_address, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.DeleteEmailIdentity] + + +# snippet-end:[python.example_code.sesv2.SESv2Wrapper.class] \ No newline at end of file diff --git a/python/example_code/sesv2/test_sesv2_email_attachments.py b/python/example_code/sesv2/test_sesv2_email_attachments.py new file mode 100644 index 00000000000..71b638d68d1 --- /dev/null +++ b/python/example_code/sesv2/test_sesv2_email_attachments.py @@ -0,0 +1,210 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Integration tests for the SESv2 Email Attachments scenario. + +These tests call real AWS SESv2 APIs and require a verified email identity. +Set the environment variable SENDER_EMAIL to a verified SES email address. +""" + +import os + +import boto3 +import pytest + +from sesv2_wrapper import SESv2Wrapper +from scenario_sesv2_email_attachments import SESv2EmailAttachmentsScenario + + +@pytest.mark.integ +def test_sesv2_wrapper_get_email_identity(): + """Test that get_email_identity returns identity info for a verified sender.""" + sender_email = os.environ.get("SENDER_EMAIL") + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + try: + result = wrapper.get_email_identity(sender_email) + assert "VerifiedForSendingStatus" in result + finally: + # No cleanup needed for read-only operation. + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_create_and_delete_template(): + """Test creating and deleting an email template.""" + wrapper = SESv2Wrapper.from_client() + template_name = "IntegTestTemplate" + try: + wrapper.create_email_template( + template_name=template_name, + subject="Test Subject for {{name}}", + html_body="

Hello {{name}}

", + text_body="Hello {{name}}", + ) + finally: + try: + wrapper.delete_email_template(template_name) + except Exception: + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_send_email_with_attachment(): + """Test sending a simple email with a file attachment.""" + sender_email = os.environ.get("SENDER_EMAIL") + recipient_email = os.environ.get("RECIPIENT_EMAIL", sender_email) + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + sample_content = b"Integration test attachment content." + + attachment = { + "RawContent": sample_content, + "FileName": "integ-test.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Integration test file", + "ContentTransferEncoding": "BASE64", + } + + try: + message_id = wrapper.send_email( + from_address=sender_email, + to_addresses=[recipient_email], + subject="SESv2 Integration Test — Attachment", + html_body="

Integration test with attachment.

", + text_body="Integration test with attachment.", + attachments=[attachment], + ) + assert message_id is not None + assert len(message_id) > 0 + finally: + # No cleanup needed for sent emails. + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_send_email_with_inline_image(): + """Test sending a simple email with an inline image.""" + sender_email = os.environ.get("SENDER_EMAIL") + recipient_email = os.environ.get("RECIPIENT_EMAIL", sender_email) + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + + # Minimal 1x1 PNG + sample_image = ( + b"\x89PNG\r\n\x1a\n" + b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01" + b"\x08\x02\x00\x00\x00\x90wS\xde" + b"\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01" + b"\x00\x05\x18\xd8N" + b"\x00\x00\x00\x00IEND\xaeB`\x82" + ) + + attachment = { + "RawContent": sample_image, + "FileName": "test-logo.png", + "ContentType": "image/png", + "ContentDisposition": "INLINE", + "ContentId": "testlogo123", + "ContentDescription": "Test logo", + "ContentTransferEncoding": "BASE64", + } + + html_body = ( + '

Test

' + 'Test Logo' + '' + ) + + try: + message_id = wrapper.send_email( + from_address=sender_email, + to_addresses=[recipient_email], + subject="SESv2 Integration Test — Inline Image", + html_body=html_body, + text_body="Inline image test — view in HTML client.", + attachments=[attachment], + ) + assert message_id is not None + assert len(message_id) > 0 + finally: + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_send_bulk_email_with_attachment(): + """Test sending bulk templated email with an attachment.""" + sender_email = os.environ.get("SENDER_EMAIL") + recipient_email = os.environ.get("RECIPIENT_EMAIL", sender_email) + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + template_name = "IntegTestBulkTemplate" + + sample_content = b"Bulk test attachment content." + attachment = { + "RawContent": sample_content, + "FileName": "bulk-test.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Bulk test file", + "ContentTransferEncoding": "BASE64", + } + + try: + wrapper.create_email_template( + template_name=template_name, + subject="Bulk Test for {{name}}", + html_body="

Hello {{name}}

Attached document.

", + text_body="Hello {{name}}, attached document.", + ) + + bulk_entries = [ + { + "Destination": {"ToAddresses": [recipient_email]}, + "ReplacementEmailContent": { + "ReplacementTemplate": { + "ReplacementTemplateData": '{"name": "TestUser"}' + } + }, + } + ] + + results = wrapper.send_bulk_email( + from_address=sender_email, + template_name=template_name, + default_template_data='{"name": "Default"}', + bulk_entries=bulk_entries, + attachments=[attachment], + ) + + assert results is not None + assert len(results) == 1 + assert results[0].get("Status") == "SUCCESS" + finally: + try: + wrapper.delete_email_template(template_name) + except Exception: + pass + + +@pytest.mark.integ +def test_sesv2_hello(capsys): + """Test the Hello SESv2 example.""" + from sesv2_hello import hello_sesv2 + + try: + hello_sesv2(boto3.client("sesv2")) + captured = capsys.readouterr() + assert "Hello, Amazon SESv2!" in captured.out + finally: + pass \ No newline at end of file From 10a6fede61d73459e21c00ba4ff03bc0ed616beb Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Tue, 14 Apr 2026 14:39:03 -0500 Subject: [PATCH 05/25] Updates to README and tools requirements. --- .tools/base_requirements.txt | 4 +- .tools/readmes/requirements_freeze.txt | 2 +- python/example_code/sesv2/README.md | 51 ++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/.tools/base_requirements.txt b/.tools/base_requirements.txt index 07e44a90fcf..9a251cb9671 100644 --- a/.tools/base_requirements.txt +++ b/.tools/base_requirements.txt @@ -1,7 +1,7 @@ -black==26.3.1 +black==24.10.0 flake8==6.1.0 mypy-extensions==1.0.0 -pathspec==1.0.0 +pathspec==0.11.2 PyYAML==6.0.1 requests==2.33.0 typer==0.15.2 diff --git a/.tools/readmes/requirements_freeze.txt b/.tools/readmes/requirements_freeze.txt index dcb659f4610..90f55ef6e05 100644 --- a/.tools/readmes/requirements_freeze.txt +++ b/.tools/readmes/requirements_freeze.txt @@ -1,5 +1,5 @@ aws_doc_sdk_examples_tools @ git+https://github.com/awsdocs/aws-doc-sdk-examples-tools@2025.41.0 -black==26.3.1 +black==24.10.0 certifi==2025.1.31 charset-normalizer==3.4.1 click==8.1.8 diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index a35582ddd21..e03ec0a6da0 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -34,17 +34,23 @@ python -m pip install -r requirements.txt +### Get started + +- [Hello Amazon SES v2 API](sesv2_hello.py#L18) (`ListEmailIdentities`) + + ### Single actions Code excerpts that show you how to call individual service functions. - [CreateContact](newsletter.py#L155) - [CreateContactList](newsletter.py#L105) -- [CreateEmailIdentity](newsletter.py#L92) -- [CreateEmailTemplate](newsletter.py#L118) +- [CreateEmailIdentity](sesv2_wrapper.py#L73) +- [CreateEmailTemplate](sesv2_wrapper.py#L109) - [DeleteContactList](newsletter.py#L258) -- [DeleteEmailIdentity](newsletter.py#L286) -- [DeleteEmailTemplate](newsletter.py#L271) +- [DeleteEmailIdentity](sesv2_wrapper.py#L321) +- [DeleteEmailTemplate](sesv2_wrapper.py#L291) +- [GetEmailIdentity](sesv2_wrapper.py#L42) - [ListContacts](newsletter.py#L198) - [SendEmail](newsletter.py#L164) @@ -53,8 +59,14 @@ Code excerpts that show you how to call individual service functions. Code examples that show you how to accomplish a specific task by calling multiple functions within the same service. +- [Email Attachments Scenario](sesv2_wrapper.py) - [Newsletter scenario](newsletter.py) +### Actions +_Actions_ are code excerpts from larger programs and must be run in context. While actions show you how to call individual service functions, you can see actions in context in their related scenarios. + +- [SendBulkEmail](sesv2_wrapper.py#L227) + @@ -74,7 +86,38 @@ To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer +#### Hello Amazon SES v2 API + +This example shows you how to get started using Amazon SES v2 API. + +``` +python sesv2_hello.py +``` + + +#### Email Attachments Scenario + +This example shows you how to send emails with attachments using Amazon SES v2 API. + +- Verify sender email identity. +- Create an email template for bulk sends. +- Send a simple email with a file attachment. +- Send a simple email with an inline image. +- Send bulk templated emails with attachments. +- Clean up resources. + + + + +Start the example by running the following at a command prompt: + +``` +python sesv2_wrapper.py +``` + + + #### Newsletter scenario From e4f8dc88b140abf7302133ff076a443bf58b46e3 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Fri, 24 Apr 2026 10:50:14 -0500 Subject: [PATCH 06/25] Updates for project structure. --- .doc_gen/metadata/sesv2_metadata.yaml | 89 +++++++++++-------- python/example_code/sesv2/README.md | 34 +++---- .../scenario_sesv2_email_attachments.py | 2 +- .../{ => attachments_scenario}/sesv2_hello.py | 0 .../sesv2_wrapper.py | 0 .../test}/test_sesv2_email_attachments.py | 5 +- .../{ => newsletter_scenario}/newsletter.py | 0 .../test}/newsletter_test.py | 3 + 8 files changed, 77 insertions(+), 56 deletions(-) rename python/example_code/sesv2/{ => attachments_scenario}/scenario_sesv2_email_attachments.py (99%) rename python/example_code/sesv2/{ => attachments_scenario}/sesv2_hello.py (100%) rename python/example_code/sesv2/{ => attachments_scenario}/sesv2_wrapper.py (100%) rename python/example_code/sesv2/{ => attachments_scenario/test}/test_sesv2_email_attachments.py (97%) rename python/example_code/sesv2/{ => newsletter_scenario}/newsletter.py (100%) rename python/example_code/sesv2/{ => newsletter_scenario/test}/newsletter_test.py (99%) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 45b53bca279..2d173075340 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -39,7 +39,7 @@ sesv2_CreateContactList: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/newsletter_scenario excerpts: - description: genai: most @@ -89,7 +89,7 @@ sesv2_CreateContact: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/newsletter_scenario excerpts: - description: genai: most @@ -104,7 +104,7 @@ sesv2_GetEmailIdentity: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: snippet_tags: @@ -172,7 +172,7 @@ sesv2_ListContacts: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/newsletter_scenario excerpts: - description: genai: most @@ -243,7 +243,14 @@ sesv2_SendEmail: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario + excerpts: + - description: Sends a message with optional attachments. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SendEmail + - sdk_version: 3 + github: python/example_code/sesv2/newsletter_scenario excerpts: - description: Sends a message to all members of the contact list. genai: most @@ -257,10 +264,6 @@ sesv2_SendEmail: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.template - - description: Sends a message with optional attachments. - snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl - - python.example_code.sesv2.SendEmail services: sesv2: {SendEmail} sesv2_CreateEmailIdentity: @@ -295,17 +298,20 @@ sesv2_CreateEmailIdentity: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: - genai: most snippet_tags: - - python.example_code.sesv2.SESv2Workflow.main - - python.example_code.sesv2.SESv2Workflow.decl + - python.example_code.sesv2.SESv2Wrapper.decl - python.example_code.sesv2.CreateEmailIdentity + - sdk_version: 3 + github: python/example_code/sesv2/newsletter_scenario + excerpts: - description: + genai: most snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailIdentity Rust: versions: @@ -350,17 +356,20 @@ sesv2_CreateEmailTemplate: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: - genai: most snippet_tags: - - python.example_code.sesv2.SESv2Workflow.main - - python.example_code.sesv2.SESv2Workflow.decl + - python.example_code.sesv2.SESv2Wrapper.decl - python.example_code.sesv2.CreateEmailTemplate + - sdk_version: 3 + github: python/example_code/sesv2/newsletter_scenario + excerpts: - description: + genai: most snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailTemplate Rust: versions: @@ -405,7 +414,7 @@ sesv2_DeleteContactList: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/newsletter_scenario excerpts: - description: genai: most @@ -456,17 +465,20 @@ sesv2_DeleteEmailIdentity: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: - genai: most snippet_tags: - - python.example_code.sesv2.SESv2Workflow.main - - python.example_code.sesv2.SESv2Workflow.decl + - python.example_code.sesv2.SESv2Wrapper.decl - python.example_code.sesv2.DeleteEmailIdentity + - sdk_version: 3 + github: python/example_code/sesv2/newsletter_scenario + excerpts: - description: + genai: most snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailIdentity Rust: versions: @@ -511,17 +523,20 @@ sesv2_DeleteEmailTemplate: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: - genai: most snippet_tags: - - python.example_code.sesv2.SESv2Workflow.main - - python.example_code.sesv2.SESv2Workflow.decl + - python.example_code.sesv2.SESv2Wrapper.decl - python.example_code.sesv2.DeleteEmailTemplate + - sdk_version: 3 + github: python/example_code/sesv2/newsletter_scenario + excerpts: - description: + genai: most snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailTemplate Rust: versions: @@ -573,7 +588,7 @@ sesv2_NewsletterWorkflow: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/newsletter_scenario excerpts: - description: genai: most @@ -620,7 +635,7 @@ sesv2_Hello: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: snippet_tags: @@ -636,7 +651,7 @@ sesv2_SendBulkEmail: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: snippet_tags: @@ -660,13 +675,13 @@ sesv2_Scenario_EmailAttachments: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - - description: Create an SESv2 wrapper class to manage operations. - snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.class - description: Run an interactive scenario demonstrating email attachments. snippet_tags: - python.example_code.sesv2.Scenario_EmailAttachments + - description: Create an SESv2 wrapper class to manage operations. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.class services: sesv2: {SendEmail, SendBulkEmail, CreateEmailIdentity, CreateEmailTemplate, GetEmailIdentity, DeleteEmailTemplate, DeleteEmailIdentity} diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index e03ec0a6da0..b29fd446d4d 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -36,36 +36,36 @@ python -m pip install -r requirements.txt ### Get started -- [Hello Amazon SES v2 API](sesv2_hello.py#L18) (`ListEmailIdentities`) +- [Hello Amazon SES v2 API](attachments_scenario/sesv2_hello.py#L18) (`ListEmailIdentities`) ### Single actions Code excerpts that show you how to call individual service functions. -- [CreateContact](newsletter.py#L155) -- [CreateContactList](newsletter.py#L105) -- [CreateEmailIdentity](sesv2_wrapper.py#L73) -- [CreateEmailTemplate](sesv2_wrapper.py#L109) -- [DeleteContactList](newsletter.py#L258) -- [DeleteEmailIdentity](sesv2_wrapper.py#L321) -- [DeleteEmailTemplate](sesv2_wrapper.py#L291) -- [GetEmailIdentity](sesv2_wrapper.py#L42) -- [ListContacts](newsletter.py#L198) -- [SendEmail](newsletter.py#L164) +- [CreateContact](newsletter_scenario/newsletter.py#L155) +- [CreateContactList](newsletter_scenario/newsletter.py#L105) +- [CreateEmailIdentity](newsletter_scenario/newsletter.py#L92) +- [CreateEmailTemplate](newsletter_scenario/newsletter.py#L118) +- [DeleteContactList](newsletter_scenario/newsletter.py#L258) +- [DeleteEmailIdentity](newsletter_scenario/newsletter.py#L286) +- [DeleteEmailTemplate](newsletter_scenario/newsletter.py#L271) +- [GetEmailIdentity](attachments_scenario/sesv2_wrapper.py#L42) +- [ListContacts](newsletter_scenario/newsletter.py#L198) +- [SendEmail](attachments_scenario/sesv2_wrapper.py#L155) ### Scenarios Code examples that show you how to accomplish a specific task by calling multiple functions within the same service. -- [Email Attachments Scenario](sesv2_wrapper.py) -- [Newsletter scenario](newsletter.py) +- [Email Attachments Scenario](attachments_scenario/scenario_sesv2_email_attachments.py) +- [Newsletter scenario](newsletter_scenario/newsletter.py) ### Actions _Actions_ are code excerpts from larger programs and must be run in context. While actions show you how to call individual service functions, you can see actions in context in their related scenarios. -- [SendBulkEmail](sesv2_wrapper.py#L227) +- [SendBulkEmail](attachments_scenario/sesv2_wrapper.py#L227) @@ -91,7 +91,7 @@ To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer This example shows you how to get started using Amazon SES v2 API. ``` -python sesv2_hello.py +python attachments_scenario/sesv2_hello.py ``` @@ -112,7 +112,7 @@ This example shows you how to send emails with attachments using Amazon SES v2 A Start the example by running the following at a command prompt: ``` -python sesv2_wrapper.py +python attachments_scenario/scenario_sesv2_email_attachments.py ``` @@ -130,7 +130,7 @@ This example shows you how to run the Amazon SES v2 API newsletter scenario. Start the example by running the following at a command prompt: ``` -python newsletter.py +python newsletter_scenario/newsletter.py ``` diff --git a/python/example_code/sesv2/scenario_sesv2_email_attachments.py b/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py similarity index 99% rename from python/example_code/sesv2/scenario_sesv2_email_attachments.py rename to python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py index 198d35275e2..e0176d86a13 100644 --- a/python/example_code/sesv2/scenario_sesv2_email_attachments.py +++ b/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py @@ -23,7 +23,7 @@ from sesv2_wrapper import SESv2Wrapper # Add relative path to include demo_tools in this code example without need for setup. -sys.path.append("../..") +sys.path.append("../../..") import demo_tools.question as q # noqa logger = logging.getLogger(__name__) diff --git a/python/example_code/sesv2/sesv2_hello.py b/python/example_code/sesv2/attachments_scenario/sesv2_hello.py similarity index 100% rename from python/example_code/sesv2/sesv2_hello.py rename to python/example_code/sesv2/attachments_scenario/sesv2_hello.py diff --git a/python/example_code/sesv2/sesv2_wrapper.py b/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py similarity index 100% rename from python/example_code/sesv2/sesv2_wrapper.py rename to python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py diff --git a/python/example_code/sesv2/test_sesv2_email_attachments.py b/python/example_code/sesv2/attachments_scenario/test/test_sesv2_email_attachments.py similarity index 97% rename from python/example_code/sesv2/test_sesv2_email_attachments.py rename to python/example_code/sesv2/attachments_scenario/test/test_sesv2_email_attachments.py index 71b638d68d1..d65ac25a5aa 100644 --- a/python/example_code/sesv2/test_sesv2_email_attachments.py +++ b/python/example_code/sesv2/attachments_scenario/test/test_sesv2_email_attachments.py @@ -9,6 +9,9 @@ """ import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import boto3 import pytest @@ -200,7 +203,7 @@ def test_sesv2_wrapper_send_bulk_email_with_attachment(): @pytest.mark.integ def test_sesv2_hello(capsys): """Test the Hello SESv2 example.""" - from sesv2_hello import hello_sesv2 + from sesv2_hello import hello_sesv2 # noqa: resolved via sys.path try: hello_sesv2(boto3.client("sesv2")) diff --git a/python/example_code/sesv2/newsletter.py b/python/example_code/sesv2/newsletter_scenario/newsletter.py similarity index 100% rename from python/example_code/sesv2/newsletter.py rename to python/example_code/sesv2/newsletter_scenario/newsletter.py diff --git a/python/example_code/sesv2/newsletter_test.py b/python/example_code/sesv2/newsletter_scenario/test/newsletter_test.py similarity index 99% rename from python/example_code/sesv2/newsletter_test.py rename to python/example_code/sesv2/newsletter_scenario/test/newsletter_test.py index 10633b9a41e..f98204e6b69 100644 --- a/python/example_code/sesv2/newsletter_test.py +++ b/python/example_code/sesv2/newsletter_scenario/test/newsletter_test.py @@ -2,12 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 import json +import os import sys from botocore.exceptions import ClientError from io import StringIO import pytest from unittest.mock import patch +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + from newsletter import ( SESv2Workflow, get_subaddress_variants, From 23c6e696046b668c010fd42c8e6d5ee3b4fa7728 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Thu, 9 Apr 2026 10:36:16 -0500 Subject: [PATCH 07/25] Initial commit of codeloom code --- .doc_gen/metadata/sesv2_metadata.yaml | 89 ++++ .gitignore | 1 + python/example_code/sesv2/requirements.txt | 3 +- .../sesv2/scenario_sesv2_email_attachments.py | 396 ++++++++++++++++++ python/example_code/sesv2/sesv2_hello.py | 77 ++++ python/example_code/sesv2/sesv2_wrapper.py | 352 ++++++++++++++++ .../sesv2/test_sesv2_email_attachments.py | 210 ++++++++++ 7 files changed, 1127 insertions(+), 1 deletion(-) create mode 100644 python/example_code/sesv2/scenario_sesv2_email_attachments.py create mode 100644 python/example_code/sesv2/sesv2_hello.py create mode 100644 python/example_code/sesv2/sesv2_wrapper.py create mode 100644 python/example_code/sesv2/test_sesv2_email_attachments.py diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 313a51113c9..45b53bca279 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -101,6 +101,15 @@ sesv2_CreateContact: sesv2: {CreateContact} sesv2_GetEmailIdentity: languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.GetEmailIdentity Rust: versions: - sdk_version: 1 @@ -248,6 +257,10 @@ sesv2_SendEmail: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.template + - description: Sends a message with optional attachments. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SendEmail services: sesv2: {SendEmail} sesv2_CreateEmailIdentity: @@ -290,6 +303,10 @@ sesv2_CreateEmailIdentity: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailIdentity + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.CreateEmailIdentity Rust: versions: - sdk_version: 1 @@ -341,6 +358,10 @@ sesv2_CreateEmailTemplate: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailTemplate + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.CreateEmailTemplate Rust: versions: - sdk_version: 1 @@ -443,6 +464,10 @@ sesv2_DeleteEmailIdentity: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailIdentity + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.DeleteEmailIdentity Rust: versions: - sdk_version: 1 @@ -494,6 +519,10 @@ sesv2_DeleteEmailTemplate: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailTemplate + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.DeleteEmailTemplate Rust: versions: - sdk_version: 1 @@ -581,3 +610,63 @@ sesv2_NewsletterWorkflow: services: sesv2: {CreateContactList, CreateContact, ListContacts, SendEmail.simple, SendEmail.template, CreateEmailIdentity, CreateEmailTemplate, DeleteContactList, DeleteEmailIdentity, DeleteEmailTemplate} + +sesv2_Hello: + title: Hello &SESv2; + title_abbrev: Hello &SESv2; + synopsis: get started using &SESv2;. + category: Hello + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.Hello + services: + sesv2: {ListEmailIdentities} +sesv2_SendBulkEmail: + title: Send bulk email with &SESv2; + title_abbrev: Send bulk email + synopsis: send bulk email with &SESv2;. + category: Actions + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SendBulkEmail + services: + sesv2: {SendBulkEmail} +sesv2_Scenario_EmailAttachments: + title: Send emails with attachments using &SESv2; + title_abbrev: Email Attachments Scenario + synopsis: send emails with attachments using &SESv2;. + synopsis_list: + - Verify sender email identity. + - Create an email template for bulk sends. + - Send a simple email with a file attachment. + - Send a simple email with an inline image. + - Send bulk templated emails with attachments. + - Clean up resources. + category: Scenarios + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: Create an SESv2 wrapper class to manage operations. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.class + - description: Run an interactive scenario demonstrating email attachments. + snippet_tags: + - python.example_code.sesv2.Scenario_EmailAttachments + services: + sesv2: {SendEmail, SendBulkEmail, CreateEmailIdentity, CreateEmailTemplate, GetEmailIdentity, DeleteEmailTemplate, DeleteEmailIdentity} diff --git a/.gitignore b/.gitignore index 657ad41f507..f9318473e2b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ kotlin/services/**/.kotlin/ .kiro/settings/ .kiro/steering/ +/codeloom_outputs diff --git a/python/example_code/sesv2/requirements.txt b/python/example_code/sesv2/requirements.txt index 621e276912d..1ce66d6ec91 100644 --- a/python/example_code/sesv2/requirements.txt +++ b/python/example_code/sesv2/requirements.txt @@ -1,2 +1,3 @@ -boto3>=1.26.79 +boto3>=1.35.0 +botocore>=1.35.0 pytest>=7.2.1 diff --git a/python/example_code/sesv2/scenario_sesv2_email_attachments.py b/python/example_code/sesv2/scenario_sesv2_email_attachments.py new file mode 100644 index 00000000000..198d35275e2 --- /dev/null +++ b/python/example_code/sesv2/scenario_sesv2_email_attachments.py @@ -0,0 +1,396 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Purpose + Shows how to use the AWS SDK for Python (Boto3) with Amazon SESv2 to + send emails with attachments. This scenario demonstrates three use cases: + 1. Send a simple email with a file attachment. + 2. Send a simple email with an inline image rendered in the HTML body. + 3. Send bulk templated emails with attachments to multiple recipients. + + The new attachment support eliminates the need for developers to construct + raw MIME messages — SES handles the MIME assembly automatically. +""" + +import json +import logging +import sys + +import boto3 +from botocore.exceptions import ClientError + +from sesv2_wrapper import SESv2Wrapper + +# Add relative path to include demo_tools in this code example without need for setup. +sys.path.append("../..") +import demo_tools.question as q # noqa + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.sesv2.Scenario_EmailAttachments] +class SESv2EmailAttachmentsScenario: + """ + Demonstrates sending emails with attachments using Amazon SESv2. + + This scenario walks through: + 1. Setting up an email identity and template. + 2. Sending a simple email with a file attachment. + 3. Sending a simple email with an inline image. + 4. Sending bulk templated emails with attachments. + 5. Cleaning up created resources. + """ + + TEMPLATE_NAME = "AttachmentDemoTemplate" + + def __init__(self, sesv2_wrapper: SESv2Wrapper) -> None: + """ + :param sesv2_wrapper: An instance of the SESv2Wrapper class. + """ + self.sesv2_wrapper = sesv2_wrapper + self.sender_email = "" + self.recipient_emails: list = [] + self.identity_was_created = False + + def run_scenario(self) -> None: + """Runs the SESv2 email attachments scenario.""" + print("-" * 88) + print("Welcome to the Amazon SESv2 Email Attachments Scenario!") + print("-" * 88) + print( + "This scenario demonstrates how to send emails with attachments\n" + "using the new SESv2 attachment support. SES handles MIME\n" + "construction automatically, so you don't need to build raw\n" + "MIME messages.\n" + ) + + try: + self._setup() + self._step1_send_email_with_attachment() + self._step2_send_email_with_inline_image() + self._step3_send_bulk_email_with_attachments() + except Exception as e: + logger.error("Scenario failed: %s", e) + print(f"\nThe scenario encountered an error: {e}") + finally: + self._cleanup() + + # ---------- Setup ---------- + + def _setup(self) -> None: + """ + Prompts for configuration, verifies the sender identity, prepares a + sample attachment, and creates an email template. + """ + print("\n--- Setup ---\n") + + # Prompt for sender and recipient addresses. + print( + "Both sender and recipient addresses must be verified if your\n" + "account is in the SES sandbox.\n" + ) + self.sender_email = q.ask( + "Enter a verified sender email address: " + ) + recipient_input = q.ask( + "Enter one or more recipient email addresses (comma-separated): " + ) + self.recipient_emails = [ + addr.strip() for addr in recipient_input.split(",") if addr.strip() + ] + + # Verify the sender identity. + print(f"\nChecking identity for {self.sender_email}...") + try: + identity_info = self.sesv2_wrapper.get_email_identity( + self.sender_email + ) + verified = identity_info.get("VerifiedForSendingStatus", False) + if verified: + print(f" ✓ {self.sender_email} is verified and ready to send.") + else: + print( + f" ⚠ {self.sender_email} exists but is not yet verified." + ) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + print( + f" Identity {self.sender_email} not found. " + "Creating it now..." + ) + result = self.sesv2_wrapper.create_email_identity( + self.sender_email + ) + self.identity_was_created = True + print( + f" Identity created. Verification status: " + f"{result.get('VerifiedForSendingStatus', False)}" + ) + print( + " Check your inbox and click the verification link " + "before continuing." + ) + q.ask("Press Enter when you have verified the address...") + else: + raise + + # Create the email template for the bulk-send step. + print("\nCreating email template for the bulk email step...") + try: + self.sesv2_wrapper.create_email_template( + template_name=self.TEMPLATE_NAME, + subject="Bulk Email with Attachment for {{name}}", + html_body=( + "

Hello {{name}}

" + "

Please find the attached document.

" + ), + text_body=( + "Hello {{name}}, Please find the attached document." + ), + ) + print(f" ✓ Template '{self.TEMPLATE_NAME}' created.\n") + except ClientError as err: + if err.response["Error"]["Code"] == "AlreadyExistsException": + print( + f" Template '{self.TEMPLATE_NAME}' already exists. " + "Using it.\n" + ) + else: + raise + + # ---------- Step 1: Simple email with file attachment ---------- + + def _step1_send_email_with_attachment(self) -> None: + """Sends a simple email with a text file attachment.""" + print("\n--- Step 1: Send a Simple Email with a File Attachment ---\n") + print( + "Creating a sample text file attachment and sending it with\n" + "the Simple email content type. SES constructs the MIME message\n" + "automatically.\n" + ) + + # Prepare a sample text file as bytes. + sample_content = b"This is a sample report attachment." + + attachment = { + "RawContent": sample_content, + "FileName": "sample-report.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Sample report text file", + "ContentTransferEncoding": "BASE64", + } + + print( + "Note: When using an AWS SDK, the SDK handles base64 encoding\n" + "automatically. Direct API callers must encode content themselves.\n" + ) + + message_id = self.sesv2_wrapper.send_email( + from_address=self.sender_email, + to_addresses=self.recipient_emails, + subject="SESv2 Attachment Demo — Simple Email with Attachment", + html_body=( + "

Attachment Demo

" + "

Please see the attached report document.

" + ), + text_body="Please see the attached report document.", + attachments=[attachment], + ) + + print(f" ✓ Email sent! MessageId: {message_id}") + print( + " SES automatically constructed the MIME message with the " + "attachment.\n" + ) + + # ---------- Step 2: Simple email with inline image ---------- + + def _step2_send_email_with_inline_image(self) -> None: + """Sends a simple email with an inline image that renders in HTML.""" + print("\n--- Step 2: Send a Simple Email with an Inline Image ---\n") + print( + "This step demonstrates INLINE disposition. The image renders\n" + "directly in the HTML body using a 'cid:' reference instead of\n" + "appearing as a downloadable attachment.\n" + ) + + # Create a minimal 1x1 red PNG (valid PNG file). + sample_image = ( + b"\x89PNG\r\n\x1a\n" # PNG signature + b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01" + b"\x08\x02\x00\x00\x00\x90wS\xde" # 1x1 RGB + b"\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01" + b"\x00\x05\x18\xd8N" # compressed data + b"\x00\x00\x00\x00IEND\xaeB`\x82" # IEND + ) + + attachment = { + "RawContent": sample_image, + "FileName": "logo.png", + "ContentType": "image/png", + "ContentDisposition": "INLINE", + "ContentId": "logo123", + "ContentDescription": "Company logo", + "ContentTransferEncoding": "BASE64", + } + + html_body = ( + "" + "

Inline Image Demo

" + "

Here is our logo:

" + 'Company Logo' + "" + ) + + message_id = self.sesv2_wrapper.send_email( + from_address=self.sender_email, + to_addresses=self.recipient_emails, + subject="SESv2 Attachment Demo — Inline Image", + html_body=html_body, + text_body=( + "Inline Image Demo — Please view this email in an " + "HTML-capable client to see the embedded image." + ), + attachments=[attachment], + ) + + print(f" ✓ Email sent! MessageId: {message_id}") + print( + " The ContentId 'logo123' is referenced in the HTML body via\n" + " 'cid:logo123', which lets the image render inline.\n" + ) + + # ---------- Step 3: Bulk templated email with attachments ---------- + + def _step3_send_bulk_email_with_attachments(self) -> None: + """Sends bulk templated emails with attachments to multiple recipients.""" + print("\n--- Step 3: Send Bulk Templated Emails with Attachments ---\n") + print( + "Using SendBulkEmail to send a templated email with an attachment\n" + "to multiple recipients in a single API call. Each recipient gets\n" + "personalized content via template data.\n" + ) + + sample_content = b"This is a sample report attachment." + + attachment = { + "RawContent": sample_content, + "FileName": "sample-report.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Sample report for bulk recipients", + "ContentTransferEncoding": "BASE64", + } + + # Build one entry per recipient with personalized names. + names = ["Alice", "Bob", "Charlie", "Diana", "Eve"] + bulk_entries = [] + for i, email in enumerate(self.recipient_emails): + name = names[i] if i < len(names) else f"Recipient{i + 1}" + bulk_entries.append( + { + "Destination": {"ToAddresses": [email]}, + "ReplacementEmailContent": { + "ReplacementTemplate": { + "ReplacementTemplateData": json.dumps( + {"name": name} + ) + } + }, + } + ) + + results = self.sesv2_wrapper.send_bulk_email( + from_address=self.sender_email, + template_name=self.TEMPLATE_NAME, + default_template_data='{"name": "Valued Customer"}', + bulk_entries=bulk_entries, + attachments=[attachment], + ) + + print(" Bulk email results:") + for idx, result in enumerate(results): + status = result.get("Status", "Unknown") + msg_id = result.get("MessageId", "N/A") + error = result.get("Error", "") + recipient = ( + self.recipient_emails[idx] + if idx < len(self.recipient_emails) + else "Unknown" + ) + print(f" {recipient}: Status={status}, MessageId={msg_id}") + if error: + print(f" Error: {error}") + + print( + "\n All recipients receive the same attachment(s) defined in\n" + " DefaultContent. Template data is personalized per recipient.\n" + ) + + # ---------- Cleanup ---------- + + def _cleanup(self) -> None: + """Deletes the email template and optionally the email identity.""" + print("\n--- Cleanup ---\n") + + # Delete the email template. + try: + self.sesv2_wrapper.delete_email_template(self.TEMPLATE_NAME) + print(f" ✓ Template '{self.TEMPLATE_NAME}' deleted.") + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + print( + f" Template '{self.TEMPLATE_NAME}' was already deleted." + ) + else: + logger.error("Failed to delete template: %s", err) + + # Optionally delete the email identity. + if self.identity_was_created and self.sender_email: + delete_identity = q.ask( + f"Delete the email identity '{self.sender_email}'? (y/n) ", + q.is_yesno, + ) + if delete_identity: + try: + self.sesv2_wrapper.delete_email_identity( + self.sender_email + ) + print( + f" ✓ Email identity '{self.sender_email}' deleted." + ) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + print( + f" Identity '{self.sender_email}' was " + "already deleted." + ) + else: + logger.error( + "Failed to delete identity: %s", err + ) + else: + print( + f" Skipping identity deletion for {self.sender_email}." + ) + else: + print( + " Sender identity was pre-existing. Skipping deletion." + ) + + print("\nAll resources have been cleaned up.") + print("-" * 88) + + +# snippet-end:[python.example_code.sesv2.Scenario_EmailAttachments] + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + try: + scenario = SESv2EmailAttachmentsScenario(SESv2Wrapper.from_client()) + scenario.run_scenario() + except Exception: + logging.exception("Something went wrong with the scenario.") \ No newline at end of file diff --git a/python/example_code/sesv2/sesv2_hello.py b/python/example_code/sesv2/sesv2_hello.py new file mode 100644 index 00000000000..c707c92d3a9 --- /dev/null +++ b/python/example_code/sesv2/sesv2_hello.py @@ -0,0 +1,77 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Purpose + Shows how to get started with Amazon SESv2 by listing email identities + associated with the account. +""" + +import logging + +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.sesv2.Hello] +def hello_sesv2(sesv2_client): + """ + Use the AWS SDK for Python (Boto3) to create an Amazon SESv2 client and + list the email identities in your account. This example uses the default + settings specified in your shared credentials and config files. + + :param sesv2_client: A Boto3 SESv2 client object. + """ + print("Hello, Amazon SESv2! Let's list your email identities:\n") + + identity_count = 0 + next_token = None + try: + while True: + kwargs = {"PageSize": 20} + if next_token: + kwargs["NextToken"] = next_token + response = sesv2_client.list_email_identities(**kwargs) + identities = response.get("EmailIdentities", []) + for identity in identities: + identity_count += 1 + identity_name = identity.get("IdentityName", "Unknown") + identity_type = identity.get("IdentityType", "Unknown") + verification_status = identity.get( + "VerificationStatus", "Unknown" + ) + sending_enabled = identity.get("SendingEnabled", False) + print( + f" Identity: {identity_name}" + f" Type: {identity_type}" + f" Status: {verification_status}" + f" Sending: {'Enabled' if sending_enabled else 'Disabled'}" + ) + next_token = response.get("NextToken") + if not next_token: + break + + if identity_count == 0: + print( + "No email identities found. " + "Use CreateEmailIdentity to add one." + ) + else: + print(f"\nFound {identity_count} email identity(ies).") + + except ClientError as err: + logger.error( + "Couldn't list email identities. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + +# snippet-end:[python.example_code.sesv2.Hello] + + +if __name__ == "__main__": + hello_sesv2(boto3.client("sesv2")) diff --git a/python/example_code/sesv2/sesv2_wrapper.py b/python/example_code/sesv2/sesv2_wrapper.py new file mode 100644 index 00000000000..870314325b7 --- /dev/null +++ b/python/example_code/sesv2/sesv2_wrapper.py @@ -0,0 +1,352 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Encapsulates Amazon SESv2 actions for sending emails with attachments. +""" + +import logging +from typing import Any, Dict, List, Optional + +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.sesv2.SESv2Wrapper.class] +# snippet-start:[python.example_code.sesv2.SESv2Wrapper.decl] +class SESv2Wrapper: + """Encapsulates Amazon SESv2 email sending actions.""" + + def __init__(self, sesv2_client: Any) -> None: + """ + Initializes the SESv2Wrapper with an SESv2 client. + + :param sesv2_client: A Boto3 SESv2 client. + """ + self.sesv2_client = sesv2_client + + @classmethod + def from_client(cls) -> "SESv2Wrapper": + """ + Creates an SESv2Wrapper instance with a default Boto3 SESv2 client. + + :return: A new SESv2Wrapper instance. + """ + sesv2_client = boto3.client("sesv2") + return cls(sesv2_client) + + # snippet-end:[python.example_code.sesv2.SESv2Wrapper.decl] + + # snippet-start:[python.example_code.sesv2.GetEmailIdentity] + def get_email_identity(self, email_address: str) -> Dict[str, Any]: + """ + Gets information about an email identity, including its verification status. + + :param email_address: The email address or domain to look up. + :return: A dictionary with identity information including verification status. + :raises ClientError: If the identity is not found (NotFoundException). + """ + try: + response = self.sesv2_client.get_email_identity( + EmailIdentity=email_address + ) + logger.info("Got email identity for %s.", email_address) + return response + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + logger.info( + "Email identity %s not found.", email_address + ) + else: + logger.error( + "Couldn't get email identity %s. Here's why: %s: %s", + email_address, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.GetEmailIdentity] + + # snippet-start:[python.example_code.sesv2.CreateEmailIdentity] + def create_email_identity(self, email_address: str) -> Dict[str, Any]: + """ + Starts the process of verifying an email identity (email address or domain). + + :param email_address: The email address or domain to verify. + :return: A dictionary with the identity type and verification status. + :raises ClientError: If the limit is exceeded (LimitExceededException). + """ + try: + response = self.sesv2_client.create_email_identity( + EmailIdentity=email_address + ) + logger.info( + "Started verification for email identity %s.", email_address + ) + return response + except ClientError as err: + if err.response["Error"]["Code"] == "LimitExceededException": + logger.error( + "Couldn't create email identity %s. You have exceeded " + "the maximum number of email identities. " + "Use an existing verified identity.", + email_address, + ) + else: + logger.error( + "Couldn't create email identity %s. Here's why: %s: %s", + email_address, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.CreateEmailIdentity] + + # snippet-start:[python.example_code.sesv2.CreateEmailTemplate] + def create_email_template( + self, + template_name: str, + subject: str, + html_body: str, + text_body: str, + ) -> None: + """ + Creates an email template for use with templated and bulk email sends. + + :param template_name: The name for the new template. + :param subject: The subject line of the template. May include {{placeholders}}. + :param html_body: The HTML body of the template. + :param text_body: The plain text body of the template. + :raises ClientError: If the template limit is exceeded (LimitExceededException). + """ + try: + self.sesv2_client.create_email_template( + TemplateName=template_name, + TemplateContent={ + "Subject": subject, + "Html": html_body, + "Text": text_body, + }, + ) + logger.info("Created email template %s.", template_name) + except ClientError as err: + if err.response["Error"]["Code"] == "LimitExceededException": + logger.error( + "Couldn't create email template %s. You have exceeded " + "the maximum number of email templates. " + "Delete unused templates first.", + template_name, + ) + else: + logger.error( + "Couldn't create email template %s. Here's why: %s: %s", + template_name, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.CreateEmailTemplate] + + # snippet-start:[python.example_code.sesv2.SendEmail] + def send_email( + self, + from_address: str, + to_addresses: List[str], + subject: str, + html_body: str, + text_body: str, + attachments: Optional[List[Dict[str, Any]]] = None, + ) -> str: + """ + Sends a simple email message with optional attachments. + + SES handles MIME construction automatically when using attachments + with the Simple content type, so developers don't need to build + raw MIME messages. + + :param from_address: The verified sender email address. + :param to_addresses: A list of recipient email addresses. + :param subject: The subject line of the email. + :param html_body: The HTML body content. + :param text_body: The plain text body content. + :param attachments: An optional list of attachment dictionaries. Each + attachment should contain 'RawContent' (bytes), 'FileName' (str), + and optionally 'ContentType', 'ContentDisposition', 'ContentId', + 'ContentDescription', and 'ContentTransferEncoding'. + :return: The MessageId of the sent email. + :raises ClientError: If the message is rejected (MessageRejected). + """ + try: + simple_message: Dict[str, Any] = { + "Subject": {"Data": subject}, + "Body": { + "Html": {"Data": html_body}, + "Text": {"Data": text_body}, + }, + } + + if attachments: + simple_message["Attachments"] = attachments + + response = self.sesv2_client.send_email( + FromEmailAddress=from_address, + Destination={"ToAddresses": to_addresses}, + Content={"Simple": simple_message}, + ) + message_id = response["MessageId"] + logger.info( + "Sent email from %s to %s. MessageId: %s", + from_address, + to_addresses, + message_id, + ) + return message_id + except ClientError as err: + if err.response["Error"]["Code"] == "MessageRejected": + logger.error( + "Message was rejected. Check that attachments use " + "supported file types and total message size is " + "under 40 MB. Details: %s", + err.response["Error"]["Message"], + ) + else: + logger.error( + "Couldn't send email. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.SendEmail] + + # snippet-start:[python.example_code.sesv2.SendBulkEmail] + def send_bulk_email( + self, + from_address: str, + template_name: str, + default_template_data: str, + bulk_entries: List[Dict[str, Any]], + attachments: Optional[List[Dict[str, Any]]] = None, + ) -> List[Dict[str, Any]]: + """ + Sends a templated email to multiple recipients in a single API call. + + All recipients receive the same attachment(s) defined in the default + content, while template data can be personalized per recipient. + + :param from_address: The verified sender email address. + :param template_name: The name of an existing email template. + :param default_template_data: Default JSON template data string. + :param bulk_entries: A list of BulkEmailEntry dicts, each containing + 'Destination' and optionally 'ReplacementEmailContent'. + :param attachments: An optional list of attachment dicts for all + recipients. + :return: A list of BulkEmailEntryResult dicts with status and MessageId. + :raises ClientError: If the message is rejected (MessageRejected). + """ + try: + template_content: Dict[str, Any] = { + "TemplateName": template_name, + "TemplateData": default_template_data, + } + + if attachments: + template_content["Attachments"] = attachments + + response = self.sesv2_client.send_bulk_email( + FromEmailAddress=from_address, + DefaultContent={"Template": template_content}, + BulkEmailEntries=bulk_entries, + ) + results = response.get("BulkEmailEntryResults", []) + logger.info( + "Sent bulk email from %s to %d recipients.", + from_address, + len(bulk_entries), + ) + return results + except ClientError as err: + if err.response["Error"]["Code"] == "MessageRejected": + logger.error( + "Bulk message was rejected. Check that the template " + "exists, attachment file types are supported, and " + "total message size is within limits. Details: %s", + err.response["Error"]["Message"], + ) + else: + logger.error( + "Couldn't send bulk email. Here's why: %s: %s", + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.SendBulkEmail] + + # snippet-start:[python.example_code.sesv2.DeleteEmailTemplate] + def delete_email_template(self, template_name: str) -> None: + """ + Deletes an email template. + + :param template_name: The name of the template to delete. + :raises ClientError: If the template is not found (NotFoundException). + """ + try: + self.sesv2_client.delete_email_template( + TemplateName=template_name + ) + logger.info("Deleted email template %s.", template_name) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + logger.info( + "Email template %s not found or already deleted.", + template_name, + ) + else: + logger.error( + "Couldn't delete email template %s. Here's why: %s: %s", + template_name, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.DeleteEmailTemplate] + + # snippet-start:[python.example_code.sesv2.DeleteEmailIdentity] + def delete_email_identity(self, email_address: str) -> None: + """ + Deletes an email identity. + + :param email_address: The email address or domain to delete. + :raises ClientError: If the identity is not found (NotFoundException). + """ + try: + self.sesv2_client.delete_email_identity( + EmailIdentity=email_address + ) + logger.info("Deleted email identity %s.", email_address) + except ClientError as err: + if err.response["Error"]["Code"] == "NotFoundException": + logger.info( + "Email identity %s not found or already deleted.", + email_address, + ) + else: + logger.error( + "Couldn't delete email identity %s. Here's why: %s: %s", + email_address, + err.response["Error"]["Code"], + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.sesv2.DeleteEmailIdentity] + + +# snippet-end:[python.example_code.sesv2.SESv2Wrapper.class] \ No newline at end of file diff --git a/python/example_code/sesv2/test_sesv2_email_attachments.py b/python/example_code/sesv2/test_sesv2_email_attachments.py new file mode 100644 index 00000000000..71b638d68d1 --- /dev/null +++ b/python/example_code/sesv2/test_sesv2_email_attachments.py @@ -0,0 +1,210 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Integration tests for the SESv2 Email Attachments scenario. + +These tests call real AWS SESv2 APIs and require a verified email identity. +Set the environment variable SENDER_EMAIL to a verified SES email address. +""" + +import os + +import boto3 +import pytest + +from sesv2_wrapper import SESv2Wrapper +from scenario_sesv2_email_attachments import SESv2EmailAttachmentsScenario + + +@pytest.mark.integ +def test_sesv2_wrapper_get_email_identity(): + """Test that get_email_identity returns identity info for a verified sender.""" + sender_email = os.environ.get("SENDER_EMAIL") + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + try: + result = wrapper.get_email_identity(sender_email) + assert "VerifiedForSendingStatus" in result + finally: + # No cleanup needed for read-only operation. + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_create_and_delete_template(): + """Test creating and deleting an email template.""" + wrapper = SESv2Wrapper.from_client() + template_name = "IntegTestTemplate" + try: + wrapper.create_email_template( + template_name=template_name, + subject="Test Subject for {{name}}", + html_body="

Hello {{name}}

", + text_body="Hello {{name}}", + ) + finally: + try: + wrapper.delete_email_template(template_name) + except Exception: + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_send_email_with_attachment(): + """Test sending a simple email with a file attachment.""" + sender_email = os.environ.get("SENDER_EMAIL") + recipient_email = os.environ.get("RECIPIENT_EMAIL", sender_email) + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + sample_content = b"Integration test attachment content." + + attachment = { + "RawContent": sample_content, + "FileName": "integ-test.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Integration test file", + "ContentTransferEncoding": "BASE64", + } + + try: + message_id = wrapper.send_email( + from_address=sender_email, + to_addresses=[recipient_email], + subject="SESv2 Integration Test — Attachment", + html_body="

Integration test with attachment.

", + text_body="Integration test with attachment.", + attachments=[attachment], + ) + assert message_id is not None + assert len(message_id) > 0 + finally: + # No cleanup needed for sent emails. + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_send_email_with_inline_image(): + """Test sending a simple email with an inline image.""" + sender_email = os.environ.get("SENDER_EMAIL") + recipient_email = os.environ.get("RECIPIENT_EMAIL", sender_email) + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + + # Minimal 1x1 PNG + sample_image = ( + b"\x89PNG\r\n\x1a\n" + b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01" + b"\x08\x02\x00\x00\x00\x90wS\xde" + b"\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01" + b"\x00\x05\x18\xd8N" + b"\x00\x00\x00\x00IEND\xaeB`\x82" + ) + + attachment = { + "RawContent": sample_image, + "FileName": "test-logo.png", + "ContentType": "image/png", + "ContentDisposition": "INLINE", + "ContentId": "testlogo123", + "ContentDescription": "Test logo", + "ContentTransferEncoding": "BASE64", + } + + html_body = ( + '

Test

' + 'Test Logo' + '' + ) + + try: + message_id = wrapper.send_email( + from_address=sender_email, + to_addresses=[recipient_email], + subject="SESv2 Integration Test — Inline Image", + html_body=html_body, + text_body="Inline image test — view in HTML client.", + attachments=[attachment], + ) + assert message_id is not None + assert len(message_id) > 0 + finally: + pass + + +@pytest.mark.integ +def test_sesv2_wrapper_send_bulk_email_with_attachment(): + """Test sending bulk templated email with an attachment.""" + sender_email = os.environ.get("SENDER_EMAIL") + recipient_email = os.environ.get("RECIPIENT_EMAIL", sender_email) + if not sender_email: + pytest.skip("SENDER_EMAIL env var not set; skipping integration test.") + + wrapper = SESv2Wrapper.from_client() + template_name = "IntegTestBulkTemplate" + + sample_content = b"Bulk test attachment content." + attachment = { + "RawContent": sample_content, + "FileName": "bulk-test.txt", + "ContentType": "text/plain", + "ContentDisposition": "ATTACHMENT", + "ContentDescription": "Bulk test file", + "ContentTransferEncoding": "BASE64", + } + + try: + wrapper.create_email_template( + template_name=template_name, + subject="Bulk Test for {{name}}", + html_body="

Hello {{name}}

Attached document.

", + text_body="Hello {{name}}, attached document.", + ) + + bulk_entries = [ + { + "Destination": {"ToAddresses": [recipient_email]}, + "ReplacementEmailContent": { + "ReplacementTemplate": { + "ReplacementTemplateData": '{"name": "TestUser"}' + } + }, + } + ] + + results = wrapper.send_bulk_email( + from_address=sender_email, + template_name=template_name, + default_template_data='{"name": "Default"}', + bulk_entries=bulk_entries, + attachments=[attachment], + ) + + assert results is not None + assert len(results) == 1 + assert results[0].get("Status") == "SUCCESS" + finally: + try: + wrapper.delete_email_template(template_name) + except Exception: + pass + + +@pytest.mark.integ +def test_sesv2_hello(capsys): + """Test the Hello SESv2 example.""" + from sesv2_hello import hello_sesv2 + + try: + hello_sesv2(boto3.client("sesv2")) + captured = capsys.readouterr() + assert "Hello, Amazon SESv2!" in captured.out + finally: + pass \ No newline at end of file From f117f337e7d31ca5f6fd6d43edbf10ebfcd0a230 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Tue, 14 Apr 2026 14:39:03 -0500 Subject: [PATCH 08/25] Updates to README and tools requirements. --- .tools/base_requirements.txt | 4 +- .tools/readmes/requirements_freeze.txt | 2 +- python/example_code/sesv2/README.md | 51 ++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/.tools/base_requirements.txt b/.tools/base_requirements.txt index 07e44a90fcf..9a251cb9671 100644 --- a/.tools/base_requirements.txt +++ b/.tools/base_requirements.txt @@ -1,7 +1,7 @@ -black==26.3.1 +black==24.10.0 flake8==6.1.0 mypy-extensions==1.0.0 -pathspec==1.0.0 +pathspec==0.11.2 PyYAML==6.0.1 requests==2.33.0 typer==0.15.2 diff --git a/.tools/readmes/requirements_freeze.txt b/.tools/readmes/requirements_freeze.txt index dcb659f4610..90f55ef6e05 100644 --- a/.tools/readmes/requirements_freeze.txt +++ b/.tools/readmes/requirements_freeze.txt @@ -1,5 +1,5 @@ aws_doc_sdk_examples_tools @ git+https://github.com/awsdocs/aws-doc-sdk-examples-tools@2025.41.0 -black==26.3.1 +black==24.10.0 certifi==2025.1.31 charset-normalizer==3.4.1 click==8.1.8 diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index a35582ddd21..e03ec0a6da0 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -34,17 +34,23 @@ python -m pip install -r requirements.txt +### Get started + +- [Hello Amazon SES v2 API](sesv2_hello.py#L18) (`ListEmailIdentities`) + + ### Single actions Code excerpts that show you how to call individual service functions. - [CreateContact](newsletter.py#L155) - [CreateContactList](newsletter.py#L105) -- [CreateEmailIdentity](newsletter.py#L92) -- [CreateEmailTemplate](newsletter.py#L118) +- [CreateEmailIdentity](sesv2_wrapper.py#L73) +- [CreateEmailTemplate](sesv2_wrapper.py#L109) - [DeleteContactList](newsletter.py#L258) -- [DeleteEmailIdentity](newsletter.py#L286) -- [DeleteEmailTemplate](newsletter.py#L271) +- [DeleteEmailIdentity](sesv2_wrapper.py#L321) +- [DeleteEmailTemplate](sesv2_wrapper.py#L291) +- [GetEmailIdentity](sesv2_wrapper.py#L42) - [ListContacts](newsletter.py#L198) - [SendEmail](newsletter.py#L164) @@ -53,8 +59,14 @@ Code excerpts that show you how to call individual service functions. Code examples that show you how to accomplish a specific task by calling multiple functions within the same service. +- [Email Attachments Scenario](sesv2_wrapper.py) - [Newsletter scenario](newsletter.py) +### Actions +_Actions_ are code excerpts from larger programs and must be run in context. While actions show you how to call individual service functions, you can see actions in context in their related scenarios. + +- [SendBulkEmail](sesv2_wrapper.py#L227) + @@ -74,7 +86,38 @@ To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer +#### Hello Amazon SES v2 API + +This example shows you how to get started using Amazon SES v2 API. + +``` +python sesv2_hello.py +``` + + +#### Email Attachments Scenario + +This example shows you how to send emails with attachments using Amazon SES v2 API. + +- Verify sender email identity. +- Create an email template for bulk sends. +- Send a simple email with a file attachment. +- Send a simple email with an inline image. +- Send bulk templated emails with attachments. +- Clean up resources. + + + + +Start the example by running the following at a command prompt: + +``` +python sesv2_wrapper.py +``` + + + #### Newsletter scenario From 0ead7b3cff3237a579ed07fc4c9fbb28156c9f82 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Fri, 24 Apr 2026 10:50:14 -0500 Subject: [PATCH 09/25] Updates for project structure. --- .doc_gen/metadata/sesv2_metadata.yaml | 89 +++++++++++-------- python/example_code/sesv2/README.md | 34 +++---- .../scenario_sesv2_email_attachments.py | 2 +- .../{ => attachments_scenario}/sesv2_hello.py | 0 .../sesv2_wrapper.py | 0 .../test}/test_sesv2_email_attachments.py | 5 +- .../{ => newsletter_scenario}/newsletter.py | 0 .../test}/newsletter_test.py | 3 + 8 files changed, 77 insertions(+), 56 deletions(-) rename python/example_code/sesv2/{ => attachments_scenario}/scenario_sesv2_email_attachments.py (99%) rename python/example_code/sesv2/{ => attachments_scenario}/sesv2_hello.py (100%) rename python/example_code/sesv2/{ => attachments_scenario}/sesv2_wrapper.py (100%) rename python/example_code/sesv2/{ => attachments_scenario/test}/test_sesv2_email_attachments.py (97%) rename python/example_code/sesv2/{ => newsletter_scenario}/newsletter.py (100%) rename python/example_code/sesv2/{ => newsletter_scenario/test}/newsletter_test.py (99%) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 45b53bca279..2d173075340 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -39,7 +39,7 @@ sesv2_CreateContactList: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/newsletter_scenario excerpts: - description: genai: most @@ -89,7 +89,7 @@ sesv2_CreateContact: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/newsletter_scenario excerpts: - description: genai: most @@ -104,7 +104,7 @@ sesv2_GetEmailIdentity: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: snippet_tags: @@ -172,7 +172,7 @@ sesv2_ListContacts: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/newsletter_scenario excerpts: - description: genai: most @@ -243,7 +243,14 @@ sesv2_SendEmail: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario + excerpts: + - description: Sends a message with optional attachments. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SendEmail + - sdk_version: 3 + github: python/example_code/sesv2/newsletter_scenario excerpts: - description: Sends a message to all members of the contact list. genai: most @@ -257,10 +264,6 @@ sesv2_SendEmail: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.template - - description: Sends a message with optional attachments. - snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl - - python.example_code.sesv2.SendEmail services: sesv2: {SendEmail} sesv2_CreateEmailIdentity: @@ -295,17 +298,20 @@ sesv2_CreateEmailIdentity: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: - genai: most snippet_tags: - - python.example_code.sesv2.SESv2Workflow.main - - python.example_code.sesv2.SESv2Workflow.decl + - python.example_code.sesv2.SESv2Wrapper.decl - python.example_code.sesv2.CreateEmailIdentity + - sdk_version: 3 + github: python/example_code/sesv2/newsletter_scenario + excerpts: - description: + genai: most snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailIdentity Rust: versions: @@ -350,17 +356,20 @@ sesv2_CreateEmailTemplate: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: - genai: most snippet_tags: - - python.example_code.sesv2.SESv2Workflow.main - - python.example_code.sesv2.SESv2Workflow.decl + - python.example_code.sesv2.SESv2Wrapper.decl - python.example_code.sesv2.CreateEmailTemplate + - sdk_version: 3 + github: python/example_code/sesv2/newsletter_scenario + excerpts: - description: + genai: most snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailTemplate Rust: versions: @@ -405,7 +414,7 @@ sesv2_DeleteContactList: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/newsletter_scenario excerpts: - description: genai: most @@ -456,17 +465,20 @@ sesv2_DeleteEmailIdentity: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: - genai: most snippet_tags: - - python.example_code.sesv2.SESv2Workflow.main - - python.example_code.sesv2.SESv2Workflow.decl + - python.example_code.sesv2.SESv2Wrapper.decl - python.example_code.sesv2.DeleteEmailIdentity + - sdk_version: 3 + github: python/example_code/sesv2/newsletter_scenario + excerpts: - description: + genai: most snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailIdentity Rust: versions: @@ -511,17 +523,20 @@ sesv2_DeleteEmailTemplate: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: - genai: most snippet_tags: - - python.example_code.sesv2.SESv2Workflow.main - - python.example_code.sesv2.SESv2Workflow.decl + - python.example_code.sesv2.SESv2Wrapper.decl - python.example_code.sesv2.DeleteEmailTemplate + - sdk_version: 3 + github: python/example_code/sesv2/newsletter_scenario + excerpts: - description: + genai: most snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailTemplate Rust: versions: @@ -573,7 +588,7 @@ sesv2_NewsletterWorkflow: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/newsletter_scenario excerpts: - description: genai: most @@ -620,7 +635,7 @@ sesv2_Hello: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: snippet_tags: @@ -636,7 +651,7 @@ sesv2_SendBulkEmail: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - description: snippet_tags: @@ -660,13 +675,13 @@ sesv2_Scenario_EmailAttachments: Python: versions: - sdk_version: 3 - github: python/example_code/sesv2 + github: python/example_code/sesv2/attachments_scenario excerpts: - - description: Create an SESv2 wrapper class to manage operations. - snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.class - description: Run an interactive scenario demonstrating email attachments. snippet_tags: - python.example_code.sesv2.Scenario_EmailAttachments + - description: Create an SESv2 wrapper class to manage operations. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.class services: sesv2: {SendEmail, SendBulkEmail, CreateEmailIdentity, CreateEmailTemplate, GetEmailIdentity, DeleteEmailTemplate, DeleteEmailIdentity} diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index e03ec0a6da0..b29fd446d4d 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -36,36 +36,36 @@ python -m pip install -r requirements.txt ### Get started -- [Hello Amazon SES v2 API](sesv2_hello.py#L18) (`ListEmailIdentities`) +- [Hello Amazon SES v2 API](attachments_scenario/sesv2_hello.py#L18) (`ListEmailIdentities`) ### Single actions Code excerpts that show you how to call individual service functions. -- [CreateContact](newsletter.py#L155) -- [CreateContactList](newsletter.py#L105) -- [CreateEmailIdentity](sesv2_wrapper.py#L73) -- [CreateEmailTemplate](sesv2_wrapper.py#L109) -- [DeleteContactList](newsletter.py#L258) -- [DeleteEmailIdentity](sesv2_wrapper.py#L321) -- [DeleteEmailTemplate](sesv2_wrapper.py#L291) -- [GetEmailIdentity](sesv2_wrapper.py#L42) -- [ListContacts](newsletter.py#L198) -- [SendEmail](newsletter.py#L164) +- [CreateContact](newsletter_scenario/newsletter.py#L155) +- [CreateContactList](newsletter_scenario/newsletter.py#L105) +- [CreateEmailIdentity](newsletter_scenario/newsletter.py#L92) +- [CreateEmailTemplate](newsletter_scenario/newsletter.py#L118) +- [DeleteContactList](newsletter_scenario/newsletter.py#L258) +- [DeleteEmailIdentity](newsletter_scenario/newsletter.py#L286) +- [DeleteEmailTemplate](newsletter_scenario/newsletter.py#L271) +- [GetEmailIdentity](attachments_scenario/sesv2_wrapper.py#L42) +- [ListContacts](newsletter_scenario/newsletter.py#L198) +- [SendEmail](attachments_scenario/sesv2_wrapper.py#L155) ### Scenarios Code examples that show you how to accomplish a specific task by calling multiple functions within the same service. -- [Email Attachments Scenario](sesv2_wrapper.py) -- [Newsletter scenario](newsletter.py) +- [Email Attachments Scenario](attachments_scenario/scenario_sesv2_email_attachments.py) +- [Newsletter scenario](newsletter_scenario/newsletter.py) ### Actions _Actions_ are code excerpts from larger programs and must be run in context. While actions show you how to call individual service functions, you can see actions in context in their related scenarios. -- [SendBulkEmail](sesv2_wrapper.py#L227) +- [SendBulkEmail](attachments_scenario/sesv2_wrapper.py#L227) @@ -91,7 +91,7 @@ To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer This example shows you how to get started using Amazon SES v2 API. ``` -python sesv2_hello.py +python attachments_scenario/sesv2_hello.py ``` @@ -112,7 +112,7 @@ This example shows you how to send emails with attachments using Amazon SES v2 A Start the example by running the following at a command prompt: ``` -python sesv2_wrapper.py +python attachments_scenario/scenario_sesv2_email_attachments.py ``` @@ -130,7 +130,7 @@ This example shows you how to run the Amazon SES v2 API newsletter scenario. Start the example by running the following at a command prompt: ``` -python newsletter.py +python newsletter_scenario/newsletter.py ``` diff --git a/python/example_code/sesv2/scenario_sesv2_email_attachments.py b/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py similarity index 99% rename from python/example_code/sesv2/scenario_sesv2_email_attachments.py rename to python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py index 198d35275e2..e0176d86a13 100644 --- a/python/example_code/sesv2/scenario_sesv2_email_attachments.py +++ b/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py @@ -23,7 +23,7 @@ from sesv2_wrapper import SESv2Wrapper # Add relative path to include demo_tools in this code example without need for setup. -sys.path.append("../..") +sys.path.append("../../..") import demo_tools.question as q # noqa logger = logging.getLogger(__name__) diff --git a/python/example_code/sesv2/sesv2_hello.py b/python/example_code/sesv2/attachments_scenario/sesv2_hello.py similarity index 100% rename from python/example_code/sesv2/sesv2_hello.py rename to python/example_code/sesv2/attachments_scenario/sesv2_hello.py diff --git a/python/example_code/sesv2/sesv2_wrapper.py b/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py similarity index 100% rename from python/example_code/sesv2/sesv2_wrapper.py rename to python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py diff --git a/python/example_code/sesv2/test_sesv2_email_attachments.py b/python/example_code/sesv2/attachments_scenario/test/test_sesv2_email_attachments.py similarity index 97% rename from python/example_code/sesv2/test_sesv2_email_attachments.py rename to python/example_code/sesv2/attachments_scenario/test/test_sesv2_email_attachments.py index 71b638d68d1..d65ac25a5aa 100644 --- a/python/example_code/sesv2/test_sesv2_email_attachments.py +++ b/python/example_code/sesv2/attachments_scenario/test/test_sesv2_email_attachments.py @@ -9,6 +9,9 @@ """ import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import boto3 import pytest @@ -200,7 +203,7 @@ def test_sesv2_wrapper_send_bulk_email_with_attachment(): @pytest.mark.integ def test_sesv2_hello(capsys): """Test the Hello SESv2 example.""" - from sesv2_hello import hello_sesv2 + from sesv2_hello import hello_sesv2 # noqa: resolved via sys.path try: hello_sesv2(boto3.client("sesv2")) diff --git a/python/example_code/sesv2/newsletter.py b/python/example_code/sesv2/newsletter_scenario/newsletter.py similarity index 100% rename from python/example_code/sesv2/newsletter.py rename to python/example_code/sesv2/newsletter_scenario/newsletter.py diff --git a/python/example_code/sesv2/newsletter_test.py b/python/example_code/sesv2/newsletter_scenario/test/newsletter_test.py similarity index 99% rename from python/example_code/sesv2/newsletter_test.py rename to python/example_code/sesv2/newsletter_scenario/test/newsletter_test.py index 10633b9a41e..f98204e6b69 100644 --- a/python/example_code/sesv2/newsletter_test.py +++ b/python/example_code/sesv2/newsletter_scenario/test/newsletter_test.py @@ -2,12 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 import json +import os import sys from botocore.exceptions import ClientError from io import StringIO import pytest from unittest.mock import patch +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + from newsletter import ( SESv2Workflow, get_subaddress_variants, From d5d4297329a1a0e1c19cda82e3f5635195eac833 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Fri, 24 Apr 2026 10:58:55 -0500 Subject: [PATCH 10/25] Update to metadata order. --- .doc_gen/metadata/sesv2_metadata.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 2d173075340..09b6e86653b 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -242,13 +242,6 @@ sesv2_SendEmail: - ruby.example_code.ses.v2.send_email Python: versions: - - sdk_version: 3 - github: python/example_code/sesv2/attachments_scenario - excerpts: - - description: Sends a message with optional attachments. - snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl - - python.example_code.sesv2.SendEmail - sdk_version: 3 github: python/example_code/sesv2/newsletter_scenario excerpts: @@ -264,6 +257,13 @@ sesv2_SendEmail: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.template + - sdk_version: 3 + github: python/example_code/sesv2/attachments_scenario + excerpts: + - description: Sends a message with optional attachments. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SendEmail services: sesv2: {SendEmail} sesv2_CreateEmailIdentity: From 9f5b1ff02b37fcbd5d3f2e905fb8cbbf0d6eab12 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Fri, 24 Apr 2026 11:27:24 -0500 Subject: [PATCH 11/25] Update to README. --- python/example_code/sesv2/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index b29fd446d4d..0a7083734e4 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -52,7 +52,7 @@ Code excerpts that show you how to call individual service functions. - [DeleteEmailTemplate](newsletter_scenario/newsletter.py#L271) - [GetEmailIdentity](attachments_scenario/sesv2_wrapper.py#L42) - [ListContacts](newsletter_scenario/newsletter.py#L198) -- [SendEmail](attachments_scenario/sesv2_wrapper.py#L155) +- [SendEmail](newsletter_scenario/newsletter.py#L164) ### Scenarios From bf96ddfb9b2b4a064ae829ae4e3003cfc59b7a0c Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Fri, 24 Apr 2026 11:37:12 -0500 Subject: [PATCH 12/25] Update to metadata --- .doc_gen/metadata/sesv2_metadata.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 09b6e86653b..725d94b0897 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -257,9 +257,6 @@ sesv2_SendEmail: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.template - - sdk_version: 3 - github: python/example_code/sesv2/attachments_scenario - excerpts: - description: Sends a message with optional attachments. snippet_tags: - python.example_code.sesv2.SESv2Wrapper.decl From 09dfd064b5d233d036a50f23f7a98f7b3db3a654 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Fri, 24 Apr 2026 12:18:10 -0500 Subject: [PATCH 13/25] Updates to metadata. --- .doc_gen/metadata/sesv2_metadata.yaml | 37 ++++++--------------------- python/example_code/sesv2/README.md | 6 +---- 2 files changed, 9 insertions(+), 34 deletions(-) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 725d94b0897..5628bb672f1 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -294,13 +294,6 @@ sesv2_CreateEmailIdentity: - sesv2.java2.newsletter.CreateEmailIdentity Python: versions: - - sdk_version: 3 - github: python/example_code/sesv2/attachments_scenario - excerpts: - - description: - snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl - - python.example_code.sesv2.CreateEmailIdentity - sdk_version: 3 github: python/example_code/sesv2/newsletter_scenario excerpts: @@ -310,6 +303,10 @@ sesv2_CreateEmailIdentity: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailIdentity + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.CreateEmailIdentity Rust: versions: - sdk_version: 1 @@ -352,13 +349,6 @@ sesv2_CreateEmailTemplate: - sesv2.java2.newsletter.CreateEmailTemplate Python: versions: - - sdk_version: 3 - github: python/example_code/sesv2/attachments_scenario - excerpts: - - description: - snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl - - python.example_code.sesv2.CreateEmailTemplate - sdk_version: 3 github: python/example_code/sesv2/newsletter_scenario excerpts: @@ -368,6 +358,10 @@ sesv2_CreateEmailTemplate: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailTemplate + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.CreateEmailTemplate Rust: versions: - sdk_version: 1 @@ -461,13 +455,6 @@ sesv2_DeleteEmailIdentity: - sesv2.java2.newsletter.DeleteEmailIdentity Python: versions: - - sdk_version: 3 - github: python/example_code/sesv2/attachments_scenario - excerpts: - - description: - snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl - - python.example_code.sesv2.DeleteEmailIdentity - sdk_version: 3 github: python/example_code/sesv2/newsletter_scenario excerpts: @@ -519,13 +506,6 @@ sesv2_DeleteEmailTemplate: - sesv2.java2.newsletter.DeleteEmailTemplate Python: versions: - - sdk_version: 3 - github: python/example_code/sesv2/attachments_scenario - excerpts: - - description: - snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl - - python.example_code.sesv2.DeleteEmailTemplate - sdk_version: 3 github: python/example_code/sesv2/newsletter_scenario excerpts: @@ -643,7 +623,6 @@ sesv2_SendBulkEmail: title: Send bulk email with &SESv2; title_abbrev: Send bulk email synopsis: send bulk email with &SESv2;. - category: Actions languages: Python: versions: diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index 0a7083734e4..89602639390 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -52,6 +52,7 @@ Code excerpts that show you how to call individual service functions. - [DeleteEmailTemplate](newsletter_scenario/newsletter.py#L271) - [GetEmailIdentity](attachments_scenario/sesv2_wrapper.py#L42) - [ListContacts](newsletter_scenario/newsletter.py#L198) +- [SendBulkEmail](attachments_scenario/sesv2_wrapper.py#L227) - [SendEmail](newsletter_scenario/newsletter.py#L164) ### Scenarios @@ -62,11 +63,6 @@ functions within the same service. - [Email Attachments Scenario](attachments_scenario/scenario_sesv2_email_attachments.py) - [Newsletter scenario](newsletter_scenario/newsletter.py) -### Actions -_Actions_ are code excerpts from larger programs and must be run in context. While actions show you how to call individual service functions, you can see actions in context in their related scenarios. - -- [SendBulkEmail](attachments_scenario/sesv2_wrapper.py#L227) - From fc286b5e763ef5f868ae45c84eedfdddecbc6786 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Fri, 24 Apr 2026 12:21:51 -0500 Subject: [PATCH 14/25] Update sesv2_metadata.yaml --- .doc_gen/metadata/sesv2_metadata.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 5628bb672f1..ef8fe5f76f4 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -620,9 +620,6 @@ sesv2_Hello: services: sesv2: {ListEmailIdentities} sesv2_SendBulkEmail: - title: Send bulk email with &SESv2; - title_abbrev: Send bulk email - synopsis: send bulk email with &SESv2;. languages: Python: versions: From 1806b6d5eb271889001b049114d614dfb0a29d34 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Fri, 24 Apr 2026 12:31:38 -0500 Subject: [PATCH 15/25] Updates to metadata. --- .doc_gen/metadata/sesv2_metadata.yaml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index ef8fe5f76f4..ccfb6231d29 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -257,10 +257,6 @@ sesv2_SendEmail: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.template - - description: Sends a message with optional attachments. - snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl - - python.example_code.sesv2.SendEmail services: sesv2: {SendEmail} sesv2_CreateEmailIdentity: @@ -303,10 +299,6 @@ sesv2_CreateEmailIdentity: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailIdentity - - description: - snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl - - python.example_code.sesv2.CreateEmailIdentity Rust: versions: - sdk_version: 1 @@ -358,10 +350,6 @@ sesv2_CreateEmailTemplate: - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailTemplate - - description: - snippet_tags: - - python.example_code.sesv2.SESv2Wrapper.decl - - python.example_code.sesv2.CreateEmailTemplate Rust: versions: - sdk_version: 1 From c4167ca1b98c61cba98f6be6712bfdf3e3c03df5 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Fri, 24 Apr 2026 14:44:05 -0500 Subject: [PATCH 16/25] Update sesv2_wrapper.py --- .../sesv2/attachments_scenario/sesv2_wrapper.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py b/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py index 870314325b7..f6d757e92f9 100644 --- a/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py +++ b/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py @@ -70,7 +70,7 @@ def get_email_identity(self, email_address: str) -> Dict[str, Any]: # snippet-end:[python.example_code.sesv2.GetEmailIdentity] - # snippet-start:[python.example_code.sesv2.CreateEmailIdentity] + # snippet-start:[python.example_code.sesv2.CreateEmailIdentityAttachment] def create_email_identity(self, email_address: str) -> Dict[str, Any]: """ Starts the process of verifying an email identity (email address or domain). @@ -104,9 +104,9 @@ def create_email_identity(self, email_address: str) -> Dict[str, Any]: ) raise - # snippet-end:[python.example_code.sesv2.CreateEmailIdentity] + # snippet-end:[python.example_code.sesv2.CreateEmailIdentityAttachment] - # snippet-start:[python.example_code.sesv2.CreateEmailTemplate] + # snippet-start:[python.example_code.sesv2.CreateEmailTemplateAttachment] def create_email_template( self, template_name: str, @@ -150,7 +150,7 @@ def create_email_template( ) raise - # snippet-end:[python.example_code.sesv2.CreateEmailTemplate] + # snippet-end:[python.example_code.sesv2.CreateEmailTemplateAttachment] # snippet-start:[python.example_code.sesv2.SendEmail] def send_email( @@ -288,7 +288,7 @@ def send_bulk_email( # snippet-end:[python.example_code.sesv2.SendBulkEmail] - # snippet-start:[python.example_code.sesv2.DeleteEmailTemplate] + # snippet-start:[python.example_code.sesv2.DeleteEmailTemplateAttachment] def delete_email_template(self, template_name: str) -> None: """ Deletes an email template. @@ -316,9 +316,9 @@ def delete_email_template(self, template_name: str) -> None: ) raise - # snippet-end:[python.example_code.sesv2.DeleteEmailTemplate] + # snippet-end:[python.example_code.sesv2.DeleteEmailTemplateAttachment] - # snippet-start:[python.example_code.sesv2.DeleteEmailIdentity] + # snippet-start:[python.example_code.sesv2.DeleteEmailIdentityAttachment] def delete_email_identity(self, email_address: str) -> None: """ Deletes an email identity. @@ -346,7 +346,7 @@ def delete_email_identity(self, email_address: str) -> None: ) raise - # snippet-end:[python.example_code.sesv2.DeleteEmailIdentity] + # snippet-end:[python.example_code.sesv2.DeleteEmailIdentityAttachment] # snippet-end:[python.example_code.sesv2.SESv2Wrapper.class] \ No newline at end of file From 7ad8301963283cb55a518eb8b0dacda0553b8430 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Fri, 24 Apr 2026 15:25:08 -0500 Subject: [PATCH 17/25] Undo requirements changes. --- .tools/base_requirements.txt | 4 ++-- .tools/readmes/requirements_freeze.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.tools/base_requirements.txt b/.tools/base_requirements.txt index 9a251cb9671..07e44a90fcf 100644 --- a/.tools/base_requirements.txt +++ b/.tools/base_requirements.txt @@ -1,7 +1,7 @@ -black==24.10.0 +black==26.3.1 flake8==6.1.0 mypy-extensions==1.0.0 -pathspec==0.11.2 +pathspec==1.0.0 PyYAML==6.0.1 requests==2.33.0 typer==0.15.2 diff --git a/.tools/readmes/requirements_freeze.txt b/.tools/readmes/requirements_freeze.txt index 90f55ef6e05..dcb659f4610 100644 --- a/.tools/readmes/requirements_freeze.txt +++ b/.tools/readmes/requirements_freeze.txt @@ -1,5 +1,5 @@ aws_doc_sdk_examples_tools @ git+https://github.com/awsdocs/aws-doc-sdk-examples-tools@2025.41.0 -black==24.10.0 +black==26.3.1 certifi==2025.1.31 charset-normalizer==3.4.1 click==8.1.8 From ffea76d926bb63902618b23617773f6467112e2a Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Fri, 24 Apr 2026 15:36:47 -0500 Subject: [PATCH 18/25] Update scenario_sesv2_email_attachments.py --- .../scenario_sesv2_email_attachments.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py b/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py index e0176d86a13..5575f86210c 100644 --- a/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py +++ b/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py @@ -108,10 +108,10 @@ def _setup(self) -> None: ) verified = identity_info.get("VerifiedForSendingStatus", False) if verified: - print(f" ✓ {self.sender_email} is verified and ready to send.") + print(f" {self.sender_email} is verified and ready to send.") else: print( - f" ⚠ {self.sender_email} exists but is not yet verified." + f" {self.sender_email} exists but is not yet verified." ) except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": @@ -149,7 +149,7 @@ def _setup(self) -> None: "Hello {{name}}, Please find the attached document." ), ) - print(f" ✓ Template '{self.TEMPLATE_NAME}' created.\n") + print(f" Template '{self.TEMPLATE_NAME}' created.\n") except ClientError as err: if err.response["Error"]["Code"] == "AlreadyExistsException": print( @@ -199,7 +199,7 @@ def _step1_send_email_with_attachment(self) -> None: attachments=[attachment], ) - print(f" ✓ Email sent! MessageId: {message_id}") + print(f" Email sent! MessageId: {message_id}") print( " SES automatically constructed the MIME message with the " "attachment.\n" @@ -256,7 +256,7 @@ def _step2_send_email_with_inline_image(self) -> None: attachments=[attachment], ) - print(f" ✓ Email sent! MessageId: {message_id}") + print(f" Email sent! MessageId: {message_id}") print( " The ContentId 'logo123' is referenced in the HTML body via\n" " 'cid:logo123', which lets the image render inline.\n" @@ -338,7 +338,7 @@ def _cleanup(self) -> None: # Delete the email template. try: self.sesv2_wrapper.delete_email_template(self.TEMPLATE_NAME) - print(f" ✓ Template '{self.TEMPLATE_NAME}' deleted.") + print(f" Template '{self.TEMPLATE_NAME}' deleted.") except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": print( @@ -359,7 +359,7 @@ def _cleanup(self) -> None: self.sender_email ) print( - f" ✓ Email identity '{self.sender_email}' deleted." + f" Email identity '{self.sender_email}' deleted." ) except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": From 9793a6af5cd6451f3e94399e6ecaa1faea2e163a Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Mon, 27 Apr 2026 14:49:25 -0500 Subject: [PATCH 19/25] Clean up messages and hello example. --- .../scenario_sesv2_email_attachments.py | 8 ++-- .../sesv2/attachments_scenario/sesv2_hello.py | 40 ++++++------------- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py b/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py index 5575f86210c..469484c6e21 100644 --- a/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py +++ b/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py @@ -34,7 +34,7 @@ class SESv2EmailAttachmentsScenario: """ Demonstrates sending emails with attachments using Amazon SESv2. - This scenario walks through: + This scenario demonstrates: 1. Setting up an email identity and template. 2. Sending a simple email with a file attachment. 3. Sending a simple email with an inline image. @@ -56,11 +56,11 @@ def __init__(self, sesv2_wrapper: SESv2Wrapper) -> None: def run_scenario(self) -> None: """Runs the SESv2 email attachments scenario.""" print("-" * 88) - print("Welcome to the Amazon SESv2 Email Attachments Scenario!") + print("Welcome to the Amazon SESv2 Email Attachments Scenario.") print("-" * 88) print( "This scenario demonstrates how to send emails with attachments\n" - "using the new SESv2 attachment support. SES handles MIME\n" + "using SESv2 attachment support. SES handles MIME\n" "construction automatically, so you don't need to build raw\n" "MIME messages.\n" ) @@ -199,7 +199,7 @@ def _step1_send_email_with_attachment(self) -> None: attachments=[attachment], ) - print(f" Email sent! MessageId: {message_id}") + print(f" Email sent. MessageId: {message_id}") print( " SES automatically constructed the MIME message with the " "attachment.\n" diff --git a/python/example_code/sesv2/attachments_scenario/sesv2_hello.py b/python/example_code/sesv2/attachments_scenario/sesv2_hello.py index c707c92d3a9..fb611b51e87 100644 --- a/python/example_code/sesv2/attachments_scenario/sesv2_hello.py +++ b/python/example_code/sesv2/attachments_scenario/sesv2_hello.py @@ -24,42 +24,26 @@ def hello_sesv2(sesv2_client): :param sesv2_client: A Boto3 SESv2 client object. """ - print("Hello, Amazon SESv2! Let's list your email identities:\n") + print("Hello, Amazon SESv2. Let's list up to 5 email identities:\n") - identity_count = 0 - next_token = None try: - while True: - kwargs = {"PageSize": 20} - if next_token: - kwargs["NextToken"] = next_token - response = sesv2_client.list_email_identities(**kwargs) - identities = response.get("EmailIdentities", []) - for identity in identities: - identity_count += 1 - identity_name = identity.get("IdentityName", "Unknown") - identity_type = identity.get("IdentityType", "Unknown") - verification_status = identity.get( - "VerificationStatus", "Unknown" - ) - sending_enabled = identity.get("SendingEnabled", False) - print( - f" Identity: {identity_name}" - f" Type: {identity_type}" - f" Status: {verification_status}" - f" Sending: {'Enabled' if sending_enabled else 'Disabled'}" - ) - next_token = response.get("NextToken") - if not next_token: - break + response = sesv2_client.list_email_identities(PageSize=5) + identities = response["EmailIdentities"] - if identity_count == 0: + if not identities: print( "No email identities found. " "Use CreateEmailIdentity to add one." ) else: - print(f"\nFound {identity_count} email identity(ies).") + for identity in identities: + print( + f" Identity: {identity['IdentityName']}" + f" Type: {identity['IdentityType']}" + f" Status: {identity['VerificationStatus']}" + f" Sending: {'Enabled' if identity['SendingEnabled'] else 'Disabled'}" + ) + print(f"\nShowing {len(identities)} email identity(ies).") except ClientError as err: logger.error( From dec58c3ca5eb7d5d100a0772c95def8af11b8a81 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Mon, 27 Apr 2026 16:32:31 -0500 Subject: [PATCH 20/25] Adding the spec and readme. --- .../features/sesv2_attachments/README.md | 51 ++++++++++++++++++ .../sesv2_attachments/SPECIFICATION.md | 54 +++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 scenarios/features/sesv2_attachments/README.md create mode 100644 scenarios/features/sesv2_attachments/SPECIFICATION.md diff --git a/scenarios/features/sesv2_attachments/README.md b/scenarios/features/sesv2_attachments/README.md new file mode 100644 index 00000000000..2748339698c --- /dev/null +++ b/scenarios/features/sesv2_attachments/README.md @@ -0,0 +1,51 @@ +# Amazon SESv2 Email Attachments Scenario + +## Overview + +This example shows how to use AWS SDKs to send emails with attachments using Amazon Simple Email Service v2 (Amazon SESv2). The scenario demonstrates the attachment support in the SESv2 `SendEmail` and `SendBulkEmail` APIs, which enables customers to include file attachments directly in simple and templated email messages without constructing raw MIME messages. SES handles the MIME construction automatically. + +[Working with email attachments in SES](https://docs.aws.amazon.com/ses/latest/dg/attachments.html) describes how to attach files such as PDFs, images, and documents to emails sent through Amazon SES. + +This scenario demonstrates the following steps and tasks: + +1. Set up a verified email identity and create an email template. +2. Send a simple email with a file attachment. + - SES automatically constructs the MIME message with the attachment. +3. Send a simple email with an inline image. + - Uses `INLINE` content disposition and `cid:` references to render images in the HTML body. +4. Send bulk templated emails with attachments to multiple recipients. + - Uses `SendBulkEmail` with personalized template data per recipient. +5. Clean up resources (delete the template and optionally the email identity). + +### Resources + +- A verified email identity (email address or domain) in Amazon SES is required to send emails. The scenario will verify or create a sender email identity. +- An email template is created during the scenario to demonstrate attachments with templated email content via `SendBulkEmail`. +- No additional AWS infrastructure (such as CloudFormation stacks) is required. + +### API actions used + +- [SendEmail](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html) +- [SendBulkEmail](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendBulkEmail.html) +- [CreateEmailIdentity](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_CreateEmailIdentity.html) +- [CreateEmailTemplate](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_CreateEmailTemplate.html) +- [GetEmailIdentity](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_GetEmailIdentity.html) +- [DeleteEmailTemplate](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_DeleteEmailTemplate.html) +- [DeleteEmailIdentity](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_DeleteEmailIdentity.html) + +## Implementations + +This example is implemented in the following languages: + +- [Python](../../../python/example_code/sesv2/attachments_scenario/README.md) + +## Additional reading + +- [What is Amazon SES?](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/Welcome.html) +- [Working with email attachments in SES](https://docs.aws.amazon.com/ses/latest/dg/attachments.html) +- [Attachment object structure](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_Attachment.html) +- [Unsupported attachment types](https://docs.aws.amazon.com/ses/latest/dg/mime-types-appendix.html) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 diff --git a/scenarios/features/sesv2_attachments/SPECIFICATION.md b/scenarios/features/sesv2_attachments/SPECIFICATION.md new file mode 100644 index 00000000000..50d3925aff6 --- /dev/null +++ b/scenarios/features/sesv2_attachments/SPECIFICATION.md @@ -0,0 +1,54 @@ +# Amazon SESv2 Email Attachments Specification + +This document contains a draft proposal for a Code Example for *Amazon SESv2 Email Attachments Scenario*, generated by the Code Examples SpecGen AI tool. The specification demonstrates the new attachment support in the SESv2 `SendEmail` and `SendBulkEmail` APIs. This feature enables customers to include file attachments (such as PDFs, images, and documents) directly in simple and templated email messages without constructing raw MIME messages. SES handles the complex MIME construction automatically, greatly simplifying the developer experience for sending emails with attachments. The following should be reviewed for accuracy and correctness before proceeding on to a final specification. + +### Resources + +- A verified email identity (email address or domain) in Amazon SES is required to send emails. The scenario will use a pre-verified sender email address. +- An email template is needed to demonstrate attachments with templated email content via `SendBulkEmail`. +- No additional AWS infrastructure (such as CloudFormation stacks) is required. + +### Relevant documentation + +* [What is Amazon SES?](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/Welcome.html) +* [Working with email attachments in SES](https://docs.aws.amazon.com/ses/latest/dg/attachments.html) +* [Sending raw email using the Amazon SES API v2](https://docs.aws.amazon.com/ses/latest/dg/send-email-raw.html) +* [Amazon SES API v2 Reference](https://docs.aws.amazon.com/ses/latest/APIReference-V2/Welcome.html) +* [Unsupported attachment types](https://docs.aws.amazon.com/ses/latest/dg/mime-types-appendix.html) +* [Attachment object structure](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_Attachment.html) + +### API Actions Used + +* [SendEmail](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html) +* [SendBulkEmail](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendBulkEmail.html) +* [CreateEmailIdentity](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_CreateEmailIdentity.html) +* [CreateEmailTemplate](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_CreateEmailTemplate.html) +* [GetEmailIdentity](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_GetEmailIdentity.html) +* [DeleteEmailTemplate](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_DeleteEmailTemplate.html) +* [DeleteEmailIdentity](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_DeleteEmailIdentity.html) + +## Hello SESv2 + +The Hello example is a separate runnable example that introduces the SESv2 service. + +- Set up the SESv2 service client. +- Call `ListEmailIdentities` to retrieve all email identities associated with the account. +- Display the list of identities along with their type and verification status. + +## Scenario + +This scenario demonstrates how to send emails with attachments using Amazon SESv2. + +## Metadata + +| action / scenario | metadata file | metadata key | +| - | - | - | +| `SESv2 Hello` | sesv2_metadata.yaml | sesv2_Hello | +| `SendEmail` | sesv2_metadata.yaml | sesv2_SendEmail | +| `SendBulkEmail` | sesv2_metadata.yaml | sesv2_SendBulkEmail | +| `CreateEmailIdentity` | sesv2_metadata.yaml | sesv2_CreateEmailIdentity | +| `CreateEmailTemplate` | sesv2_metadata.yaml | sesv2_CreateEmailTemplate | +| `GetEmailIdentity` | sesv2_metadata.yaml | sesv2_GetEmailIdentity | +| `DeleteEmailTemplate` | sesv2_metadata.yaml | sesv2_DeleteEmailTemplate | +| `DeleteEmailIdentity` | sesv2_metadata.yaml | sesv2_DeleteEmailIdentity | +| `SESv2 Email Attachments Scenario` | sesv2_metadata.yaml | sesv2_Scenario_EmailAttachments | From 406737843a973392ece7493f468643274bd55e11 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Tue, 28 Apr 2026 09:23:52 -0500 Subject: [PATCH 21/25] Updates to spec and readme. --- .doc_gen/metadata/sesv2_metadata.yaml | 7 +++++++ python/example_code/sesv2/README.md | 2 +- .../sesv2/attachments_scenario/sesv2_wrapper.py | 4 ++-- .../features/sesv2_attachments/SPECIFICATION.md | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index ccfb6231d29..6549e868c00 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -242,6 +242,13 @@ sesv2_SendEmail: - ruby.example_code.ses.v2.send_email Python: versions: + - sdk_version: 3 + github: python/example_code/sesv2/attachments_scenario + excerpts: + - description: Sends a message with optional attachments. + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.SendEmailAttachment - sdk_version: 3 github: python/example_code/sesv2/newsletter_scenario excerpts: diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index 89602639390..8c7fe423498 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -53,7 +53,7 @@ Code excerpts that show you how to call individual service functions. - [GetEmailIdentity](attachments_scenario/sesv2_wrapper.py#L42) - [ListContacts](newsletter_scenario/newsletter.py#L198) - [SendBulkEmail](attachments_scenario/sesv2_wrapper.py#L227) -- [SendEmail](newsletter_scenario/newsletter.py#L164) +- [SendEmail](attachments_scenario/sesv2_wrapper.py#L155) ### Scenarios diff --git a/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py b/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py index f6d757e92f9..a793695aecf 100644 --- a/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py +++ b/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py @@ -152,7 +152,7 @@ def create_email_template( # snippet-end:[python.example_code.sesv2.CreateEmailTemplateAttachment] - # snippet-start:[python.example_code.sesv2.SendEmail] + # snippet-start:[python.example_code.sesv2.SendEmailAttachment] def send_email( self, from_address: str, @@ -222,7 +222,7 @@ def send_email( ) raise - # snippet-end:[python.example_code.sesv2.SendEmail] + # snippet-end:[python.example_code.sesv2.SendEmailAttachment] # snippet-start:[python.example_code.sesv2.SendBulkEmail] def send_bulk_email( diff --git a/scenarios/features/sesv2_attachments/SPECIFICATION.md b/scenarios/features/sesv2_attachments/SPECIFICATION.md index 50d3925aff6..f9ddb843f1f 100644 --- a/scenarios/features/sesv2_attachments/SPECIFICATION.md +++ b/scenarios/features/sesv2_attachments/SPECIFICATION.md @@ -39,6 +39,21 @@ The Hello example is a separate runnable example that introduces the SESv2 servi This scenario demonstrates how to send emails with attachments using Amazon SESv2. +## Errors + +| action | Error | Handling | +|-----------------------|------------------------|----------------------------------------------------------------------------------------------| +| `GetEmailIdentity` | `NotFoundException` | Identity not found. Prompt to create a new one. | +| `CreateEmailIdentity` | `LimitExceededException` | Notify the user that the maximum number of email identities has been reached. | +| `CreateEmailTemplate` | `AlreadyExistsException` | Skip creation and use the existing template. | +| `CreateEmailTemplate` | `LimitExceededException` | Notify the user that the maximum number of email templates has been reached. | +| `SendEmail` | `MessageRejected` | Notify the user to check attachment file types and total message size (under 40 MB). | +| `SendBulkEmail` | `MessageRejected` | Notify the user to check the template, attachment file types, and total message size limits. | +| `DeleteEmailTemplate` | `NotFoundException` | Notify the user the template was already deleted. | +| `DeleteEmailIdentity` | `NotFoundException` | Notify the user the identity was already deleted. | + +--- + ## Metadata | action / scenario | metadata file | metadata key | From 2f9701a3ca58a33e4d0cddf1c9ddbd712712d8f2 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Tue, 28 Apr 2026 09:29:29 -0500 Subject: [PATCH 22/25] Metadata fixes --- .doc_gen/metadata/sesv2_metadata.yaml | 30 ++++++++++++++++++- .../attachments_scenario/sesv2_wrapper.py | 4 +-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 6549e868c00..2d18d6b4fbe 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -109,7 +109,7 @@ sesv2_GetEmailIdentity: - description: snippet_tags: - python.example_code.sesv2.SESv2Wrapper.decl - - python.example_code.sesv2.GetEmailIdentity + - python.example_code.sesv2.GetEmailIdentityAttachment Rust: versions: - sdk_version: 1 @@ -297,6 +297,13 @@ sesv2_CreateEmailIdentity: - sesv2.java2.newsletter.CreateEmailIdentity Python: versions: + - sdk_version: 3 + github: python/example_code/sesv2/attachments_scenario + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.CreateEmailIdentityAttachment - sdk_version: 3 github: python/example_code/sesv2/newsletter_scenario excerpts: @@ -348,6 +355,13 @@ sesv2_CreateEmailTemplate: - sesv2.java2.newsletter.CreateEmailTemplate Python: versions: + - sdk_version: 3 + github: python/example_code/sesv2/attachments_scenario + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.CreateEmailTemplateAttachment - sdk_version: 3 github: python/example_code/sesv2/newsletter_scenario excerpts: @@ -450,6 +464,13 @@ sesv2_DeleteEmailIdentity: - sesv2.java2.newsletter.DeleteEmailIdentity Python: versions: + - sdk_version: 3 + github: python/example_code/sesv2/attachments_scenario + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.DeleteEmailIdentityAttachment - sdk_version: 3 github: python/example_code/sesv2/newsletter_scenario excerpts: @@ -501,6 +522,13 @@ sesv2_DeleteEmailTemplate: - sesv2.java2.newsletter.DeleteEmailTemplate Python: versions: + - sdk_version: 3 + github: python/example_code/sesv2/attachments_scenario + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Wrapper.decl + - python.example_code.sesv2.DeleteEmailTemplateAttachment - sdk_version: 3 github: python/example_code/sesv2/newsletter_scenario excerpts: diff --git a/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py b/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py index a793695aecf..a5ed35bc7e9 100644 --- a/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py +++ b/python/example_code/sesv2/attachments_scenario/sesv2_wrapper.py @@ -39,7 +39,7 @@ def from_client(cls) -> "SESv2Wrapper": # snippet-end:[python.example_code.sesv2.SESv2Wrapper.decl] - # snippet-start:[python.example_code.sesv2.GetEmailIdentity] + # snippet-start:[python.example_code.sesv2.GetEmailIdentityAttachment] def get_email_identity(self, email_address: str) -> Dict[str, Any]: """ Gets information about an email identity, including its verification status. @@ -68,7 +68,7 @@ def get_email_identity(self, email_address: str) -> Dict[str, Any]: ) raise - # snippet-end:[python.example_code.sesv2.GetEmailIdentity] + # snippet-end:[python.example_code.sesv2.GetEmailIdentityAttachment] # snippet-start:[python.example_code.sesv2.CreateEmailIdentityAttachment] def create_email_identity(self, email_address: str) -> Dict[str, Any]: From 5aaca3919a91dba1f55a664c0e7debfd423c6089 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Tue, 28 Apr 2026 09:43:48 -0500 Subject: [PATCH 23/25] Metadata cleanup. --- python/example_code/sesv2/README.md | 8 ++++---- .../scenario_sesv2_email_attachments.py | 10 +++++----- scenarios/features/sesv2_attachments/SPECIFICATION.md | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index 8c7fe423498..6db397f80c8 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -45,11 +45,11 @@ Code excerpts that show you how to call individual service functions. - [CreateContact](newsletter_scenario/newsletter.py#L155) - [CreateContactList](newsletter_scenario/newsletter.py#L105) -- [CreateEmailIdentity](newsletter_scenario/newsletter.py#L92) -- [CreateEmailTemplate](newsletter_scenario/newsletter.py#L118) +- [CreateEmailIdentity](attachments_scenario/sesv2_wrapper.py#L73) +- [CreateEmailTemplate](attachments_scenario/sesv2_wrapper.py#L109) - [DeleteContactList](newsletter_scenario/newsletter.py#L258) -- [DeleteEmailIdentity](newsletter_scenario/newsletter.py#L286) -- [DeleteEmailTemplate](newsletter_scenario/newsletter.py#L271) +- [DeleteEmailIdentity](attachments_scenario/sesv2_wrapper.py#L321) +- [DeleteEmailTemplate](attachments_scenario/sesv2_wrapper.py#L291) - [GetEmailIdentity](attachments_scenario/sesv2_wrapper.py#L42) - [ListContacts](newsletter_scenario/newsletter.py#L198) - [SendBulkEmail](attachments_scenario/sesv2_wrapper.py#L227) diff --git a/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py b/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py index 469484c6e21..8b3c2d740a0 100644 --- a/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py +++ b/python/example_code/sesv2/attachments_scenario/scenario_sesv2_email_attachments.py @@ -10,7 +10,7 @@ 3. Send bulk templated emails with attachments to multiple recipients. The new attachment support eliminates the need for developers to construct - raw MIME messages — SES handles the MIME assembly automatically. + raw MIME messages. SES handles the MIME assembly automatically. """ import json @@ -190,7 +190,7 @@ def _step1_send_email_with_attachment(self) -> None: message_id = self.sesv2_wrapper.send_email( from_address=self.sender_email, to_addresses=self.recipient_emails, - subject="SESv2 Attachment Demo — Simple Email with Attachment", + subject="SESv2 Attachment Demo - Simple Email with Attachment", html_body=( "

Attachment Demo

" "

Please see the attached report document.

" @@ -247,16 +247,16 @@ def _step2_send_email_with_inline_image(self) -> None: message_id = self.sesv2_wrapper.send_email( from_address=self.sender_email, to_addresses=self.recipient_emails, - subject="SESv2 Attachment Demo — Inline Image", + subject="SESv2 Attachment Demo - Inline Image", html_body=html_body, text_body=( - "Inline Image Demo — Please view this email in an " + "Inline Image Demo - Please view this email in an " "HTML-capable client to see the embedded image." ), attachments=[attachment], ) - print(f" Email sent! MessageId: {message_id}") + print(f" Email sent. MessageId: {message_id}") print( " The ContentId 'logo123' is referenced in the HTML body via\n" " 'cid:logo123', which lets the image render inline.\n" diff --git a/scenarios/features/sesv2_attachments/SPECIFICATION.md b/scenarios/features/sesv2_attachments/SPECIFICATION.md index f9ddb843f1f..be2c4fbf0ff 100644 --- a/scenarios/features/sesv2_attachments/SPECIFICATION.md +++ b/scenarios/features/sesv2_attachments/SPECIFICATION.md @@ -32,7 +32,7 @@ This document contains a draft proposal for a Code Example for *Amazon SESv2 Ema The Hello example is a separate runnable example that introduces the SESv2 service. - Set up the SESv2 service client. -- Call `ListEmailIdentities` to retrieve all email identities associated with the account. +- Call `ListEmailIdentities` to retrieve up to 5 email identities associated with the account. - Display the list of identities along with their type and verification status. ## Scenario From 41cced0a7e5b127a1a21d5f33009a81e46c2501d Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Wed, 29 Apr 2026 08:35:58 -0500 Subject: [PATCH 24/25] Updates from feedback. --- .doc_gen/metadata/sesv2_metadata.yaml | 1 - scenarios/features/sesv2_attachments/README.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 2d18d6b4fbe..7a130fbc5d3 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -660,7 +660,6 @@ sesv2_Scenario_EmailAttachments: title_abbrev: Email Attachments Scenario synopsis: send emails with attachments using &SESv2;. synopsis_list: - - Verify sender email identity. - Create an email template for bulk sends. - Send a simple email with a file attachment. - Send a simple email with an inline image. diff --git a/scenarios/features/sesv2_attachments/README.md b/scenarios/features/sesv2_attachments/README.md index 2748339698c..7efe851b09e 100644 --- a/scenarios/features/sesv2_attachments/README.md +++ b/scenarios/features/sesv2_attachments/README.md @@ -43,6 +43,7 @@ This example is implemented in the following languages: - [What is Amazon SES?](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/Welcome.html) - [Working with email attachments in SES](https://docs.aws.amazon.com/ses/latest/dg/attachments.html) +- [Moving out of the Amazon SES sandbox](https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html) - [Attachment object structure](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_Attachment.html) - [Unsupported attachment types](https://docs.aws.amazon.com/ses/latest/dg/mime-types-appendix.html) From a6129287246e9278c8233ced6f6a9fa1e467980b Mon Sep 17 00:00:00 2001 From: Rachel Hagerman Date: Wed, 29 Apr 2026 08:39:02 -0500 Subject: [PATCH 25/25] Update README.md --- python/example_code/sesv2/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index 6db397f80c8..8a9c02da473 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -95,7 +95,6 @@ python attachments_scenario/sesv2_hello.py This example shows you how to send emails with attachments using Amazon SES v2 API. -- Verify sender email identity. - Create an email template for bulk sends. - Send a simple email with a file attachment. - Send a simple email with an inline image.