There are more AWS SDK examples available in the AWS Doc SDK Examples
Amazon SES API v2 examples using SDK for Python (Boto3)
The following code examples show you how to perform actions and implement common scenarios by using the AWS SDK for Python (Boto3) with Amazon SES API v2.
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.
Scenarios are code examples that show you how to accomplish specific tasks by calling multiple functions within a service or combined with other AWS services.
Each example includes a link to the complete source code, where you can find instructions on how to set up and run the code in context.
Get started
The following code example shows how to get started using Amazon SES API v2.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. 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 up to 5 email identities:\n") try: response = sesv2_client.list_email_identities(PageSize=5) identities = response["EmailIdentities"] if not identities: print( "No email identities found. " "Use CreateEmailIdentity to add one." ) else: 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( "Couldn't list email identities. Here's why: %s: %s", err.response["Error"]["Code"], err.response["Error"]["Message"], ) raise-
For API details, see ListEmailIdentities in AWS SDK for Python (Boto3) API Reference.
-
Actions
The following code example shows how to use CreateConfigurationSet.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. Create a configuration set to use in the Amazon SES API v2 search-and-filter scenario.
class SearchFilterWrapper: """Encapsulates Amazon SES v2 search-and-filter operations.""" def __init__(self, sesv2_client: boto3.client): """ Initializes the SearchFilterWrapper with an Amazon SES v2 client. :param sesv2_client: A Boto3 Amazon SES v2 client. """ self.sesv2_client = sesv2_client @classmethod def from_client(cls) -> "SearchFilterWrapper": """ Creates a SearchFilterWrapper instance with a default Boto3 SES v2 client. :return: An initialized SearchFilterWrapper instance. """ sesv2_client = boto3.client("sesv2") return cls(sesv2_client) def create_configuration_set(self, configuration_set_name: str) -> bool: """ Creates a configuration set with the specified name. :param configuration_set_name: The name for the new configuration set. :return: True if the configuration set was created, or False if a configuration set with that name already existed. """ try: self.sesv2_client.create_configuration_set( ConfigurationSetName=configuration_set_name ) logger.info("Created configuration set '%s'.", configuration_set_name) return True except ClientError as err: if err.response["Error"]["Code"] == "AlreadyExistsException": logger.warning( "Configuration set '%s' already exists. Skipping creation.", configuration_set_name, ) return False else: logger.error( "Error creating configuration set '%s': %s", configuration_set_name, err.response["Error"]["Message"], ) raise-
For API details, see CreateConfigurationSet in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use CreateContact.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. def main(): """ The main function that orchestrates the execution of the workflow. """ print(INTRO) ses_client = boto3.client("sesv2") workflow = SESv2Workflow(ses_client) try: workflow.prepare_application() workflow.gather_subscriber_email_addresses() workflow.send_coupon_newsletter() workflow.monitor_and_review() except ClientError as e: print_error(e) workflow.clean_up() class SESv2Workflow: """ A class to manage the SES v2 Coupon Newsletter Workflow. """ def __init__(self, ses_client, sleep=True): self.ses_client = ses_client self.sleep = sleep try: # Create a new contact self.ses_client.create_contact( ContactListName=CONTACT_LIST_NAME, EmailAddress=email ) print(f"Contact with email '{email}' created successfully.") # Send the welcome email self.ses_client.send_email( FromEmailAddress=self.verified_email, Destination={"ToAddresses": [email]}, Content={ "Simple": { "Subject": { "Data": "Welcome to the Weekly Coupons Newsletter" }, "Body": { "Text": {"Data": welcome_text}, "Html": {"Data": welcome_html}, }, } }, ) print(f"Welcome email sent to '{email}'.") if self.sleep: # 1 email per second in sandbox mode, remove in production. sleep(1.1) except ClientError as e: # If the contact already exists, skip and proceed if e.response["Error"]["Code"] == "AlreadyExistsException": print(f"Contact with email '{email}' already exists. Skipping...") else: raise e-
For API details, see CreateContact in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use CreateContactList.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. def main(): """ The main function that orchestrates the execution of the workflow. """ print(INTRO) ses_client = boto3.client("sesv2") workflow = SESv2Workflow(ses_client) try: workflow.prepare_application() workflow.gather_subscriber_email_addresses() workflow.send_coupon_newsletter() workflow.monitor_and_review() except ClientError as e: print_error(e) workflow.clean_up() class SESv2Workflow: """ A class to manage the SES v2 Coupon Newsletter Workflow. """ def __init__(self, ses_client, sleep=True): self.ses_client = ses_client self.sleep = sleep try: self.ses_client.create_contact_list(ContactListName=CONTACT_LIST_NAME) print(f"Contact list '{CONTACT_LIST_NAME}' created successfully.") except ClientError as e: # If the contact list already exists, skip and proceed if e.response["Error"]["Code"] == "AlreadyExistsException": print(f"Contact list '{CONTACT_LIST_NAME}' already exists.") else: raise e-
For API details, see CreateContactList in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use CreateEmailIdentity.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. def main(): """ The main function that orchestrates the execution of the workflow. """ print(INTRO) ses_client = boto3.client("sesv2") workflow = SESv2Workflow(ses_client) try: workflow.prepare_application() workflow.gather_subscriber_email_addresses() workflow.send_coupon_newsletter() workflow.monitor_and_review() except ClientError as e: print_error(e) workflow.clean_up() class SESv2Workflow: """ A class to manage the SES v2 Coupon Newsletter Workflow. """ def __init__(self, ses_client, sleep=True): self.ses_client = ses_client self.sleep = sleep try: self.ses_client.create_email_identity(EmailIdentity=self.verified_email) print(f"Email identity '{self.verified_email}' created successfully.") except ClientError as e: # If the email identity already exists, skip and proceed if e.response["Error"]["Code"] == "AlreadyExistsException": print(f"Email identity '{self.verified_email}' already exists.") else: raise e-
For API details, see CreateEmailIdentity in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use CreateEmailTemplate.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. def main(): """ The main function that orchestrates the execution of the workflow. """ print(INTRO) ses_client = boto3.client("sesv2") workflow = SESv2Workflow(ses_client) try: workflow.prepare_application() workflow.gather_subscriber_email_addresses() workflow.send_coupon_newsletter() workflow.monitor_and_review() except ClientError as e: print_error(e) workflow.clean_up() class SESv2Workflow: """ A class to manage the SES v2 Coupon Newsletter Workflow. """ def __init__(self, ses_client, sleep=True): self.ses_client = ses_client self.sleep = sleep try: template_content = { "Subject": "Weekly Coupons Newsletter", "Html": load_file_content("coupon-newsletter.html"), "Text": load_file_content("coupon-newsletter.txt"), } self.ses_client.create_email_template( TemplateName=TEMPLATE_NAME, TemplateContent=template_content ) print(f"Email template '{TEMPLATE_NAME}' created successfully.") except ClientError as e: # If the template already exists, skip and proceed if e.response["Error"]["Code"] == "AlreadyExistsException": print(f"Email template '{TEMPLATE_NAME}' already exists.") else: raise e-
For API details, see CreateEmailTemplate in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use CreateTenant.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. Create a tenant to use in the Amazon SES API v2 search-and-filter scenario.
class SearchFilterWrapper: """Encapsulates Amazon SES v2 search-and-filter operations.""" def __init__(self, sesv2_client: boto3.client): """ Initializes the SearchFilterWrapper with an Amazon SES v2 client. :param sesv2_client: A Boto3 Amazon SES v2 client. """ self.sesv2_client = sesv2_client @classmethod def from_client(cls) -> "SearchFilterWrapper": """ Creates a SearchFilterWrapper instance with a default Boto3 SES v2 client. :return: An initialized SearchFilterWrapper instance. """ sesv2_client = boto3.client("sesv2") return cls(sesv2_client) def create_tenant(self, tenant_name: str) -> Dict[str, Any]: """ Creates a tenant with the specified name. :param tenant_name: The name of the tenant to create. :return: A dictionary containing TenantName, TenantId, TenantArn, and CreatedTimestamp. """ try: response = self.sesv2_client.create_tenant(TenantName=tenant_name) logger.info("Created tenant '%s'.", tenant_name) return response except ClientError as err: if err.response["Error"]["Code"] == "BadRequestException": logger.error( "Bad request when creating tenant '%s': %s", tenant_name, err.response["Error"]["Message"], ) else: logger.error( "Error creating tenant '%s': %s", tenant_name, err.response["Error"]["Message"], ) raise-
For API details, see CreateTenant in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use DeleteConfigurationSet.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. Delete a configuration set that was created by the Amazon SES API v2 search-and-filter scenario.
class SearchFilterWrapper: """Encapsulates Amazon SES v2 search-and-filter operations.""" def __init__(self, sesv2_client: boto3.client): """ Initializes the SearchFilterWrapper with an Amazon SES v2 client. :param sesv2_client: A Boto3 Amazon SES v2 client. """ self.sesv2_client = sesv2_client @classmethod def from_client(cls) -> "SearchFilterWrapper": """ Creates a SearchFilterWrapper instance with a default Boto3 SES v2 client. :return: An initialized SearchFilterWrapper instance. """ sesv2_client = boto3.client("sesv2") return cls(sesv2_client) def delete_configuration_set(self, configuration_set_name: str) -> None: """ Deletes a configuration set. :param configuration_set_name: The name of the configuration set to delete. """ try: self.sesv2_client.delete_configuration_set( ConfigurationSetName=configuration_set_name ) logger.info("Deleted configuration set '%s'.", configuration_set_name) except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": logger.warning( "Configuration set '%s' not found. Skipping deletion.", configuration_set_name, ) else: logger.error( "Error deleting configuration set '%s': %s", configuration_set_name, err.response["Error"]["Message"], ) raise-
For API details, see DeleteConfigurationSet in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use DeleteContactList.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. def main(): """ The main function that orchestrates the execution of the workflow. """ print(INTRO) ses_client = boto3.client("sesv2") workflow = SESv2Workflow(ses_client) try: workflow.prepare_application() workflow.gather_subscriber_email_addresses() workflow.send_coupon_newsletter() workflow.monitor_and_review() except ClientError as e: print_error(e) workflow.clean_up() class SESv2Workflow: """ A class to manage the SES v2 Coupon Newsletter Workflow. """ def __init__(self, ses_client, sleep=True): self.ses_client = ses_client self.sleep = sleep try: self.ses_client.delete_contact_list(ContactListName=CONTACT_LIST_NAME) print(f"Contact list '{CONTACT_LIST_NAME}' deleted successfully.") except ClientError as e: # If the contact list doesn't exist, skip and proceed if e.response["Error"]["Code"] == "NotFoundException": print(f"Contact list '{CONTACT_LIST_NAME}' does not exist.") else: print(e)-
For API details, see DeleteContactList in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use DeleteEmailIdentity.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. def main(): """ The main function that orchestrates the execution of the workflow. """ print(INTRO) ses_client = boto3.client("sesv2") workflow = SESv2Workflow(ses_client) try: workflow.prepare_application() workflow.gather_subscriber_email_addresses() workflow.send_coupon_newsletter() workflow.monitor_and_review() except ClientError as e: print_error(e) workflow.clean_up() class SESv2Workflow: """ A class to manage the SES v2 Coupon Newsletter Workflow. """ def __init__(self, ses_client, sleep=True): self.ses_client = ses_client self.sleep = sleep try: self.ses_client.delete_email_identity(EmailIdentity=self.verified_email) print(f"Email identity '{self.verified_email}' deleted successfully.") except ClientError as e: # If the email identity doesn't exist, skip and proceed if e.response["Error"]["Code"] == "NotFoundException": print(f"Email identity '{self.verified_email}' does not exist.") else: print(e)-
For API details, see DeleteEmailIdentity in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use DeleteEmailTemplate.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. def main(): """ The main function that orchestrates the execution of the workflow. """ print(INTRO) ses_client = boto3.client("sesv2") workflow = SESv2Workflow(ses_client) try: workflow.prepare_application() workflow.gather_subscriber_email_addresses() workflow.send_coupon_newsletter() workflow.monitor_and_review() except ClientError as e: print_error(e) workflow.clean_up() class SESv2Workflow: """ A class to manage the SES v2 Coupon Newsletter Workflow. """ def __init__(self, ses_client, sleep=True): self.ses_client = ses_client self.sleep = sleep try: self.ses_client.delete_email_template(TemplateName=TEMPLATE_NAME) print(f"Email template '{TEMPLATE_NAME}' deleted successfully.") except ClientError as e: # If the email template doesn't exist, skip and proceed if e.response["Error"]["Code"] == "NotFoundException": print(f"Email template '{TEMPLATE_NAME}' does not exist.") else: print(e)-
For API details, see DeleteEmailTemplate in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use DeleteTenant.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. Delete a tenant that was created by the Amazon SES API v2 search-and-filter scenario.
class SearchFilterWrapper: """Encapsulates Amazon SES v2 search-and-filter operations.""" def __init__(self, sesv2_client: boto3.client): """ Initializes the SearchFilterWrapper with an Amazon SES v2 client. :param sesv2_client: A Boto3 Amazon SES v2 client. """ self.sesv2_client = sesv2_client @classmethod def from_client(cls) -> "SearchFilterWrapper": """ Creates a SearchFilterWrapper instance with a default Boto3 SES v2 client. :return: An initialized SearchFilterWrapper instance. """ sesv2_client = boto3.client("sesv2") return cls(sesv2_client) def delete_tenant(self, tenant_name: str) -> None: """ Deletes a tenant. :param tenant_name: The name of the tenant to delete. """ try: self.sesv2_client.delete_tenant(TenantName=tenant_name) logger.info("Deleted tenant '%s'.", tenant_name) except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": logger.warning( "Tenant '%s' not found. Skipping deletion.", tenant_name, ) else: logger.error( "Error deleting tenant '%s': %s", tenant_name, err.response["Error"]["Message"], ) raise-
For API details, see DeleteTenant in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use GetEmailIdentity.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. 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) 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-
For API details, see GetEmailIdentity in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use ListConfigurationSets.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. Filter configuration sets by name using the Amazon SES API v2 filter support.
class SearchFilterWrapper: """Encapsulates Amazon SES v2 search-and-filter operations.""" def __init__(self, sesv2_client: boto3.client): """ Initializes the SearchFilterWrapper with an Amazon SES v2 client. :param sesv2_client: A Boto3 Amazon SES v2 client. """ self.sesv2_client = sesv2_client @classmethod def from_client(cls) -> "SearchFilterWrapper": """ Creates a SearchFilterWrapper instance with a default Boto3 SES v2 client. :return: An initialized SearchFilterWrapper instance. """ sesv2_client = boto3.client("sesv2") return cls(sesv2_client) def list_configuration_sets( self, filter_params: Optional[Dict[str, str]] = None, page_size: Optional[int] = None, ) -> List[str]: """ Lists configuration sets associated with the account in the current region. Supports optional filter parameters and pagination. :param filter_params: Optional dictionary of filter key-value pairs (e.g., {"CONFIGURATION_SET_NAME_CONTAINS": "marketing"}). :param page_size: Optional number of results per page. :return: A list of configuration set name strings. """ try: # ListConfigurationSets has no registered Boto3 paginator, so # pagination is handled manually using the NextToken field. configuration_sets = list() next_token = None while True: kwargs = dict() if filter_params is not None: kwargs["Filter"] = filter_params if page_size is not None: kwargs["PageSize"] = page_size if next_token is not None: kwargs["NextToken"] = next_token response = self.sesv2_client.list_configuration_sets(**kwargs) configuration_sets.extend(response.get("ConfigurationSets", list())) next_token = response.get("NextToken") if not next_token: break logger.info("Listed %d configuration sets.", len(configuration_sets)) return configuration_sets except ClientError as err: if err.response["Error"]["Code"] == "BadRequestException": logger.error( "Bad request when listing configuration sets: %s", err.response["Error"]["Message"], ) else: logger.error( "Error listing configuration sets: %s", err.response["Error"]["Message"], ) raise-
For API details, see ListConfigurationSets in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use ListContacts.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. def main(): """ The main function that orchestrates the execution of the workflow. """ print(INTRO) ses_client = boto3.client("sesv2") workflow = SESv2Workflow(ses_client) try: workflow.prepare_application() workflow.gather_subscriber_email_addresses() workflow.send_coupon_newsletter() workflow.monitor_and_review() except ClientError as e: print_error(e) workflow.clean_up() class SESv2Workflow: """ A class to manage the SES v2 Coupon Newsletter Workflow. """ def __init__(self, ses_client, sleep=True): self.ses_client = ses_client self.sleep = sleep try: contacts_response = self.ses_client.list_contacts( ContactListName=CONTACT_LIST_NAME ) except ClientError as e: if e.response["Error"]["Code"] == "NotFoundException": print(f"Contact list '{CONTACT_LIST_NAME}' does not exist.") return else: raise e-
For API details, see ListContacts in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use ListEmailIdentities.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. Filter email identities by name using the Amazon SES API v2 filter support.
class SearchFilterWrapper: """Encapsulates Amazon SES v2 search-and-filter operations.""" def __init__(self, sesv2_client: boto3.client): """ Initializes the SearchFilterWrapper with an Amazon SES v2 client. :param sesv2_client: A Boto3 Amazon SES v2 client. """ self.sesv2_client = sesv2_client @classmethod def from_client(cls) -> "SearchFilterWrapper": """ Creates a SearchFilterWrapper instance with a default Boto3 SES v2 client. :return: An initialized SearchFilterWrapper instance. """ sesv2_client = boto3.client("sesv2") return cls(sesv2_client) def list_email_identities( self, filter_params: Optional[Dict[str, str]] = None, page_size: Optional[int] = None, ) -> List[Dict[str, Any]]: """ Lists email identities associated with the account. Supports optional filter parameters and pagination. :param filter_params: Optional dictionary of filter key-value pairs (e.g., {"IDENTITY_NAME_CONTAINS": "example"}). :param page_size: Optional number of results per page. :return: A list of identity info dictionaries. """ try: # ListEmailIdentities has no registered Boto3 paginator, so # pagination is handled manually using the NextToken field. identities = list() next_token = None while True: kwargs = dict() if filter_params is not None: kwargs["Filter"] = filter_params if page_size is not None: kwargs["PageSize"] = page_size if next_token is not None: kwargs["NextToken"] = next_token response = self.sesv2_client.list_email_identities(**kwargs) identities.extend(response.get("EmailIdentities", list())) next_token = response.get("NextToken") if not next_token: break logger.info("Listed %d email identities.", len(identities)) return identities except ClientError as err: if err.response["Error"]["Code"] == "BadRequestException": logger.error( "Bad request when listing email identities: %s", err.response["Error"]["Message"], ) else: logger.error( "Error listing email identities: %s", err.response["Error"]["Message"], ) raise-
For API details, see ListEmailIdentities in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use ListTenants.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. Filter tenants by name using the Amazon SES API v2 filter support.
class SearchFilterWrapper: """Encapsulates Amazon SES v2 search-and-filter operations.""" def __init__(self, sesv2_client: boto3.client): """ Initializes the SearchFilterWrapper with an Amazon SES v2 client. :param sesv2_client: A Boto3 Amazon SES v2 client. """ self.sesv2_client = sesv2_client @classmethod def from_client(cls) -> "SearchFilterWrapper": """ Creates a SearchFilterWrapper instance with a default Boto3 SES v2 client. :return: An initialized SearchFilterWrapper instance. """ sesv2_client = boto3.client("sesv2") return cls(sesv2_client) def list_tenants( self, filter_params: Optional[Dict[str, str]] = None, page_size: Optional[int] = None, ) -> List[Dict[str, Any]]: """ Lists tenants associated with the account. Supports optional filter parameters and pagination. :param filter_params: Optional dictionary of filter key-value pairs (e.g., {"TENANT_NAME_CONTAINS": "customer"}). :param page_size: Optional number of results per page. :return: A list of tenant info dictionaries. """ try: # Pagination is handled manually using the NextToken field to keep # the three list methods consistent. The sibling list operations # (configuration sets, email identities) have no registered Boto3 # paginator, so the same manual approach is used here. tenants = list() next_token = None while True: kwargs = dict() if filter_params is not None: kwargs["Filter"] = filter_params if page_size is not None: kwargs["PageSize"] = page_size if next_token is not None: kwargs["NextToken"] = next_token response = self.sesv2_client.list_tenants(**kwargs) tenants.extend(response.get("Tenants", list())) next_token = response.get("NextToken") if not next_token: break logger.info("Listed %d tenants.", len(tenants)) return tenants except ClientError as err: if err.response["Error"]["Code"] == "TooManyRequestsException": logger.error( "Too many requests when listing tenants: %s", err.response["Error"]["Message"], ) else: logger.error( "Error listing tenants: %s", err.response["Error"]["Message"], ) raise-
For API details, see ListTenants in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use SendBulkEmail.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. 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) 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-
For API details, see SendBulkEmail in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to use SendEmail.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. Sends a message to all members of the contact list.
def main(): """ The main function that orchestrates the execution of the workflow. """ print(INTRO) ses_client = boto3.client("sesv2") workflow = SESv2Workflow(ses_client) try: workflow.prepare_application() workflow.gather_subscriber_email_addresses() workflow.send_coupon_newsletter() workflow.monitor_and_review() except ClientError as e: print_error(e) workflow.clean_up() class SESv2Workflow: """ A class to manage the SES v2 Coupon Newsletter Workflow. """ def __init__(self, ses_client, sleep=True): self.ses_client = ses_client self.sleep = sleep self.ses_client.send_email( FromEmailAddress=self.verified_email, Destination={"ToAddresses": [email]}, Content={ "Simple": { "Subject": { "Data": "Welcome to the Weekly Coupons Newsletter" }, "Body": { "Text": {"Data": welcome_text}, "Html": {"Data": welcome_html}, }, } }, ) print(f"Welcome email sent to '{email}'.")Sends a message to all members of the contact list using a template.
def main(): """ The main function that orchestrates the execution of the workflow. """ print(INTRO) ses_client = boto3.client("sesv2") workflow = SESv2Workflow(ses_client) try: workflow.prepare_application() workflow.gather_subscriber_email_addresses() workflow.send_coupon_newsletter() workflow.monitor_and_review() except ClientError as e: print_error(e) workflow.clean_up() class SESv2Workflow: """ A class to manage the SES v2 Coupon Newsletter Workflow. """ def __init__(self, ses_client, sleep=True): self.ses_client = ses_client self.sleep = sleep self.ses_client.send_email( FromEmailAddress=self.verified_email, Destination={"ToAddresses": [email_address]}, Content={ "Template": { "TemplateName": TEMPLATE_NAME, "TemplateData": coupon_items, } }, ListManagementOptions={"ContactListName": CONTACT_LIST_NAME}, )-
For API details, see SendEmail in AWS SDK for Python (Boto3) API Reference.
-
Scenarios
The following code example shows how to send emails with attachments using Amazon SES API v2.
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.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. Run an interactive scenario demonstrating email attachments.
class SESv2EmailAttachmentsScenario: """ Demonstrates sending emails with attachments using Amazon SESv2. 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. 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 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=( "<h1>Hello {{name}}</h1>" "<p>Please find the attached document.</p>" ), 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=( "<h1>Attachment Demo</h1>" "<p>Please see the attached <b>report document</b>.</p>" ), 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 = ( "<html><body>" "<h1>Inline Image Demo</h1>" "<p>Here is our logo:</p>" '<img src="cid:logo123" alt="Company Logo">' "</body></html>" ) 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)Create an SESv2 wrapper class to manage operations.
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) 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 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 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 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 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 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 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-
For API details, see the following topics in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to run the Amazon SES API v2 newsletter scenario.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. def main(): """ The main function that orchestrates the execution of the workflow. """ print(INTRO) ses_client = boto3.client("sesv2") workflow = SESv2Workflow(ses_client) try: workflow.prepare_application() workflow.gather_subscriber_email_addresses() workflow.send_coupon_newsletter() workflow.monitor_and_review() except ClientError as e: print_error(e) workflow.clean_up() class SESv2Workflow: """ A class to manage the SES v2 Coupon Newsletter Workflow. """ def __init__(self, ses_client, sleep=True): self.ses_client = ses_client self.sleep = sleep try: self.ses_client.create_contact_list(ContactListName=CONTACT_LIST_NAME) print(f"Contact list '{CONTACT_LIST_NAME}' created successfully.") except ClientError as e: # If the contact list already exists, skip and proceed if e.response["Error"]["Code"] == "AlreadyExistsException": print(f"Contact list '{CONTACT_LIST_NAME}' already exists.") else: raise e try: # Create a new contact self.ses_client.create_contact( ContactListName=CONTACT_LIST_NAME, EmailAddress=email ) print(f"Contact with email '{email}' created successfully.") # Send the welcome email self.ses_client.send_email( FromEmailAddress=self.verified_email, Destination={"ToAddresses": [email]}, Content={ "Simple": { "Subject": { "Data": "Welcome to the Weekly Coupons Newsletter" }, "Body": { "Text": {"Data": welcome_text}, "Html": {"Data": welcome_html}, }, } }, ) print(f"Welcome email sent to '{email}'.") if self.sleep: # 1 email per second in sandbox mode, remove in production. sleep(1.1) except ClientError as e: # If the contact already exists, skip and proceed if e.response["Error"]["Code"] == "AlreadyExistsException": print(f"Contact with email '{email}' already exists. Skipping...") else: raise e try: contacts_response = self.ses_client.list_contacts( ContactListName=CONTACT_LIST_NAME ) except ClientError as e: if e.response["Error"]["Code"] == "NotFoundException": print(f"Contact list '{CONTACT_LIST_NAME}' does not exist.") return else: raise e self.ses_client.send_email( FromEmailAddress=self.verified_email, Destination={"ToAddresses": [email]}, Content={ "Simple": { "Subject": { "Data": "Welcome to the Weekly Coupons Newsletter" }, "Body": { "Text": {"Data": welcome_text}, "Html": {"Data": welcome_html}, }, } }, ) print(f"Welcome email sent to '{email}'.") self.ses_client.send_email( FromEmailAddress=self.verified_email, Destination={"ToAddresses": [email_address]}, Content={ "Template": { "TemplateName": TEMPLATE_NAME, "TemplateData": coupon_items, } }, ListManagementOptions={"ContactListName": CONTACT_LIST_NAME}, ) try: self.ses_client.create_email_identity(EmailIdentity=self.verified_email) print(f"Email identity '{self.verified_email}' created successfully.") except ClientError as e: # If the email identity already exists, skip and proceed if e.response["Error"]["Code"] == "AlreadyExistsException": print(f"Email identity '{self.verified_email}' already exists.") else: raise e try: template_content = { "Subject": "Weekly Coupons Newsletter", "Html": load_file_content("coupon-newsletter.html"), "Text": load_file_content("coupon-newsletter.txt"), } self.ses_client.create_email_template( TemplateName=TEMPLATE_NAME, TemplateContent=template_content ) print(f"Email template '{TEMPLATE_NAME}' created successfully.") except ClientError as e: # If the template already exists, skip and proceed if e.response["Error"]["Code"] == "AlreadyExistsException": print(f"Email template '{TEMPLATE_NAME}' already exists.") else: raise e try: self.ses_client.delete_contact_list(ContactListName=CONTACT_LIST_NAME) print(f"Contact list '{CONTACT_LIST_NAME}' deleted successfully.") except ClientError as e: # If the contact list doesn't exist, skip and proceed if e.response["Error"]["Code"] == "NotFoundException": print(f"Contact list '{CONTACT_LIST_NAME}' does not exist.") else: print(e) try: self.ses_client.delete_email_identity(EmailIdentity=self.verified_email) print(f"Email identity '{self.verified_email}' deleted successfully.") except ClientError as e: # If the email identity doesn't exist, skip and proceed if e.response["Error"]["Code"] == "NotFoundException": print(f"Email identity '{self.verified_email}' does not exist.") else: print(e) try: self.ses_client.delete_email_template(TemplateName=TEMPLATE_NAME) print(f"Email template '{TEMPLATE_NAME}' deleted successfully.") except ClientError as e: # If the email template doesn't exist, skip and proceed if e.response["Error"]["Code"] == "NotFoundException": print(f"Email template '{TEMPLATE_NAME}' does not exist.") else: print(e)-
For API details, see the following topics in AWS SDK for Python (Boto3) API Reference.
-
The following code example shows how to:
Create multiple configuration sets, email identities, and tenants.
List all configuration sets (unfiltered).
Filter configuration sets by name substring.
List all email identities (unfiltered).
Filter email identities by name substring.
List all tenants (unfiltered).
Filter tenants by name substring.
Demonstrate pagination with filters.
Clean up all created resources.
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository
. Run the Search and Filter SES resources scenario.
class SearchFilterBasicsScenario: """Runs the SES v2 Search and Filter Basics scenario.""" def __init__(self, sesv2_wrapper: SearchFilterWrapper): """ :param sesv2_wrapper: An instance of SearchFilterWrapper that provides the Amazon SES v2 operations used in this scenario. """ self.sesv2_wrapper = sesv2_wrapper def run_scenario(self) -> None: """Orchestrates the full scenario.""" print("-" * 88) print("Welcome to the Amazon SES v2 Search and Filter Basics scenario.") print("-" * 88) print( "This demo shows how to use the new filter parameters on\n" "ListConfigurationSets, ListEmailIdentities, and ListTenants to\n" "narrow results server-side." ) print() created_config_sets = list() created_identities = list() created_tenants = list() try: # --- Setup --- # Each setup helper adds resources to its list as they're created, # so cleanup can delete them even if setup fails partway through. print("--- Setup: Creating resources ---") self._setup_configuration_sets(created_config_sets) self._setup_email_identities(created_identities) self._setup_tenants(created_tenants) self._wait_for_filtered_results( created_config_sets, created_identities, created_tenants ) print() # --- Step 1: Unfiltered configuration sets --- print("--- Step 1: List ALL configuration sets (unfiltered) ---") all_config_sets = self.sesv2_wrapper.list_configuration_sets() print(f"Found {len(all_config_sets)} configuration set(s):") for name in all_config_sets: print(f" - {name}") print() # --- Step 2: Filtered configuration sets --- print( "--- Step 2: Filter configuration sets " f'(name contains "{CONFIG_SET_FILTER}") ---' ) filtered_config_sets = self.sesv2_wrapper.list_configuration_sets( filter_params={"CONFIGURATION_SET_NAME_CONTAINS": CONFIG_SET_FILTER} ) print( f"Found {len(filtered_config_sets)} of {len(all_config_sets)} " "configuration set(s):" ) for name in filtered_config_sets: print(f" - {name}") print() # --- Step 3: Unfiltered email identities --- print("--- Step 3: List ALL email identities (unfiltered) ---") all_identities = self.sesv2_wrapper.list_email_identities() print(f"Found {len(all_identities)} email identity(ies):") for identity in all_identities: print( f" - {identity.get('IdentityName', 'N/A')} " f"({identity.get('IdentityType', 'N/A')}, " f"verification={identity.get('VerificationStatus', 'N/A')})" ) print() # --- Step 4: Filtered email identities --- print( "--- Step 4: Filter email identities " f'(name contains "{IDENTITY_FILTER}") ---' ) filtered_identities = self.sesv2_wrapper.list_email_identities( filter_params={"IDENTITY_NAME_CONTAINS": IDENTITY_FILTER} ) print( f"Found {len(filtered_identities)} of {len(all_identities)} " "email identity(ies):" ) for identity in filtered_identities: print( f" - {identity.get('IdentityName', 'N/A')} " f"({identity.get('IdentityType', 'N/A')})" ) print() # --- Step 5: Unfiltered tenants --- print("--- Step 5: List ALL tenants (unfiltered) ---") all_tenants = self.sesv2_wrapper.list_tenants() print(f"Found {len(all_tenants)} tenant(s):") for tenant in all_tenants: print( f" - {tenant.get('TenantName', 'N/A')} " f"(ID: {tenant.get('TenantId', 'N/A')})" ) print() # --- Step 6: Filtered tenants --- print(f'--- Step 6: Filter tenants (name contains "{TENANT_FILTER}") ---') filtered_tenants = self.sesv2_wrapper.list_tenants( filter_params={"TENANT_NAME_CONTAINS": TENANT_FILTER} ) print(f"Found {len(filtered_tenants)} of {len(all_tenants)} tenant(s):") for tenant in filtered_tenants: print( f" - {tenant.get('TenantName', 'N/A')} " f"(ID: {tenant.get('TenantId', 'N/A')})" ) print() # --- Step 7: Pagination with filters --- print("--- Step 7: Page through filtered configuration sets ---") print( "Listing configuration sets whose name contains " f'"{CONFIG_SET_FILTER}" with PageSize=1.\n' "Each response holds one result and a NextToken that requests " "the next page." ) page_number = 0 next_token = None while True: page, next_token = self.sesv2_wrapper.list_configuration_sets_page( {"CONFIGURATION_SET_NAME_CONTAINS": CONFIG_SET_FILTER}, page_size=1, next_token=next_token, ) page_number += 1 print(f" Page {page_number}: {', '.join(page) or '(empty)'}") if not next_token: break print() print("-" * 88) print("Scenario demonstration complete!") print("-" * 88) finally: # --- Cleanup --- print() print("--- Cleanup: Removing resources created during setup ---") self._cleanup(created_config_sets, created_identities, created_tenants) print("-" * 88) # ------------------------------------------------------------------ # Setup helpers # ------------------------------------------------------------------ def _setup_configuration_sets(self, created: list) -> None: """ Creates configuration sets for the scenario and adds each new one to created. A configuration set that already exists isn't added, so cleanup won't delete it. """ for name in CONFIGURATION_SET_NAMES: if self.sesv2_wrapper.create_configuration_set(name): created.append(name) print(f" Created configuration set: {name}") else: print(f" Configuration set {name} already exists. Using it as is.") def _setup_email_identities(self, created: list) -> None: """Creates email identities and adds each new one to created.""" for identity in EMAIL_IDENTITIES: try: response = self.sesv2_wrapper.create_email_identity(identity) created.append(identity) identity_type = response.get("IdentityType", "UNKNOWN") verified = response.get("VerifiedForSendingStatus", False) print( f" Created email identity: {identity} " f"(type={identity_type}, verified={verified})" ) except ClientError: logger.warning("Could not create identity '%s'. Continuing.", identity) def _setup_tenants(self, created: list) -> None: """Creates tenants and adds each new one to created.""" for name in TENANT_NAMES: try: response = self.sesv2_wrapper.create_tenant(name) created.append(name) tenant_id = response.get("TenantId", "N/A") print(f" Created tenant: {name} (ID: {tenant_id})") except ClientError: logger.warning("Could not create tenant '%s'. Continuing.", name) def _wait_for_filtered_results( self, config_set_names: list, identity_names: list, tenant_names: list ) -> None: """ Repeats the scenario's filtered list calls until each one includes the new resources that match its filter. Each resource type is checked with its own filtered call, because one list operation being up to date doesn't mean the others are. """ expected_config_sets = {n for n in config_set_names if CONFIG_SET_FILTER in n} expected_identities = {i for i in identity_names if IDENTITY_FILTER in i} expected_tenants = {n for n in tenant_names if TENANT_FILTER in n} print("Waiting for the new resources to appear in filtered results...") for attempt in range(1, FILTER_WAIT_ATTEMPTS + 1): config_sets = set( self.sesv2_wrapper.list_configuration_sets( filter_params={"CONFIGURATION_SET_NAME_CONTAINS": CONFIG_SET_FILTER} ) ) identities = { i["IdentityName"] for i in self.sesv2_wrapper.list_email_identities( filter_params={"IDENTITY_NAME_CONTAINS": IDENTITY_FILTER} ) } tenants = { t["TenantName"] for t in self.sesv2_wrapper.list_tenants( filter_params={"TENANT_NAME_CONTAINS": TENANT_FILTER} ) } if ( expected_config_sets <= config_sets and expected_identities <= identities and expected_tenants <= tenants ): print(" The new resources are available in filtered results.") return if attempt < FILTER_WAIT_ATTEMPTS: time.sleep(FILTER_WAIT_DELAY_SECONDS) print( " Some new resources still aren't in filtered results. " "The filter steps may show fewer results than expected." ) # ------------------------------------------------------------------ # Cleanup # ------------------------------------------------------------------ def _cleanup( self, config_set_names: list, identity_names: list, tenant_names: list, ) -> None: """ Deletes all resources created during setup. Each delete is attempted on its own, so one failure doesn't leave the rest in the account. """ not_deleted = list() for kind, names, delete in ( ( "configuration set", config_set_names, self.sesv2_wrapper.delete_configuration_set, ), ( "email identity", identity_names, self.sesv2_wrapper.delete_email_identity, ), ("tenant", tenant_names, self.sesv2_wrapper.delete_tenant), ): for name in names: try: delete(name) print(f" Deleted {kind}: {name}") except ClientError as err: print( f" Couldn't delete {kind} {name}: " f"{err.response['Error']['Message']}" ) not_deleted.append(f"{kind} {name}") if not_deleted: print("These resources weren't deleted. Delete them manually:") for item in not_deleted: print(f" - {item}") else: print("Cleanup complete.")A wrapper class for the Amazon SES API v2 search-and-filter operations.
class SearchFilterWrapper: """Encapsulates Amazon SES v2 search-and-filter operations.""" def __init__(self, sesv2_client: boto3.client): """ Initializes the SearchFilterWrapper with an Amazon SES v2 client. :param sesv2_client: A Boto3 Amazon SES v2 client. """ self.sesv2_client = sesv2_client @classmethod def from_client(cls) -> "SearchFilterWrapper": """ Creates a SearchFilterWrapper instance with a default Boto3 SES v2 client. :return: An initialized SearchFilterWrapper instance. """ sesv2_client = boto3.client("sesv2") return cls(sesv2_client) def list_configuration_sets( self, filter_params: Optional[Dict[str, str]] = None, page_size: Optional[int] = None, ) -> List[str]: """ Lists configuration sets associated with the account in the current region. Supports optional filter parameters and pagination. :param filter_params: Optional dictionary of filter key-value pairs (e.g., {"CONFIGURATION_SET_NAME_CONTAINS": "marketing"}). :param page_size: Optional number of results per page. :return: A list of configuration set name strings. """ try: # ListConfigurationSets has no registered Boto3 paginator, so # pagination is handled manually using the NextToken field. configuration_sets = list() next_token = None while True: kwargs = dict() if filter_params is not None: kwargs["Filter"] = filter_params if page_size is not None: kwargs["PageSize"] = page_size if next_token is not None: kwargs["NextToken"] = next_token response = self.sesv2_client.list_configuration_sets(**kwargs) configuration_sets.extend(response.get("ConfigurationSets", list())) next_token = response.get("NextToken") if not next_token: break logger.info("Listed %d configuration sets.", len(configuration_sets)) return configuration_sets except ClientError as err: if err.response["Error"]["Code"] == "BadRequestException": logger.error( "Bad request when listing configuration sets: %s", err.response["Error"]["Message"], ) else: logger.error( "Error listing configuration sets: %s", err.response["Error"]["Message"], ) raise def list_email_identities( self, filter_params: Optional[Dict[str, str]] = None, page_size: Optional[int] = None, ) -> List[Dict[str, Any]]: """ Lists email identities associated with the account. Supports optional filter parameters and pagination. :param filter_params: Optional dictionary of filter key-value pairs (e.g., {"IDENTITY_NAME_CONTAINS": "example"}). :param page_size: Optional number of results per page. :return: A list of identity info dictionaries. """ try: # ListEmailIdentities has no registered Boto3 paginator, so # pagination is handled manually using the NextToken field. identities = list() next_token = None while True: kwargs = dict() if filter_params is not None: kwargs["Filter"] = filter_params if page_size is not None: kwargs["PageSize"] = page_size if next_token is not None: kwargs["NextToken"] = next_token response = self.sesv2_client.list_email_identities(**kwargs) identities.extend(response.get("EmailIdentities", list())) next_token = response.get("NextToken") if not next_token: break logger.info("Listed %d email identities.", len(identities)) return identities except ClientError as err: if err.response["Error"]["Code"] == "BadRequestException": logger.error( "Bad request when listing email identities: %s", err.response["Error"]["Message"], ) else: logger.error( "Error listing email identities: %s", err.response["Error"]["Message"], ) raise def list_tenants( self, filter_params: Optional[Dict[str, str]] = None, page_size: Optional[int] = None, ) -> List[Dict[str, Any]]: """ Lists tenants associated with the account. Supports optional filter parameters and pagination. :param filter_params: Optional dictionary of filter key-value pairs (e.g., {"TENANT_NAME_CONTAINS": "customer"}). :param page_size: Optional number of results per page. :return: A list of tenant info dictionaries. """ try: # Pagination is handled manually using the NextToken field to keep # the three list methods consistent. The sibling list operations # (configuration sets, email identities) have no registered Boto3 # paginator, so the same manual approach is used here. tenants = list() next_token = None while True: kwargs = dict() if filter_params is not None: kwargs["Filter"] = filter_params if page_size is not None: kwargs["PageSize"] = page_size if next_token is not None: kwargs["NextToken"] = next_token response = self.sesv2_client.list_tenants(**kwargs) tenants.extend(response.get("Tenants", list())) next_token = response.get("NextToken") if not next_token: break logger.info("Listed %d tenants.", len(tenants)) return tenants except ClientError as err: if err.response["Error"]["Code"] == "TooManyRequestsException": logger.error( "Too many requests when listing tenants: %s", err.response["Error"]["Message"], ) else: logger.error( "Error listing tenants: %s", err.response["Error"]["Message"], ) raise def create_configuration_set(self, configuration_set_name: str) -> bool: """ Creates a configuration set with the specified name. :param configuration_set_name: The name for the new configuration set. :return: True if the configuration set was created, or False if a configuration set with that name already existed. """ try: self.sesv2_client.create_configuration_set( ConfigurationSetName=configuration_set_name ) logger.info("Created configuration set '%s'.", configuration_set_name) return True except ClientError as err: if err.response["Error"]["Code"] == "AlreadyExistsException": logger.warning( "Configuration set '%s' already exists. Skipping creation.", configuration_set_name, ) return False else: logger.error( "Error creating configuration set '%s': %s", configuration_set_name, err.response["Error"]["Message"], ) raise def create_email_identity(self, email_identity: str) -> Dict[str, Any]: """ Creates an email identity (email address or domain) for verification. :param email_identity: The email address or domain to verify. :return: A dictionary with identity type, verification status, and DKIM attributes. """ try: response = self.sesv2_client.create_email_identity( EmailIdentity=email_identity ) logger.info("Created email identity '%s'.", email_identity) return response except ClientError as err: if err.response["Error"]["Code"] == "TooManyRequestsException": logger.error( "Too many requests when creating email identity '%s': %s", email_identity, err.response["Error"]["Message"], ) else: logger.error( "Error creating email identity '%s': %s", email_identity, err.response["Error"]["Message"], ) raise def create_tenant(self, tenant_name: str) -> Dict[str, Any]: """ Creates a tenant with the specified name. :param tenant_name: The name of the tenant to create. :return: A dictionary containing TenantName, TenantId, TenantArn, and CreatedTimestamp. """ try: response = self.sesv2_client.create_tenant(TenantName=tenant_name) logger.info("Created tenant '%s'.", tenant_name) return response except ClientError as err: if err.response["Error"]["Code"] == "BadRequestException": logger.error( "Bad request when creating tenant '%s': %s", tenant_name, err.response["Error"]["Message"], ) else: logger.error( "Error creating tenant '%s': %s", tenant_name, err.response["Error"]["Message"], ) raise def delete_configuration_set(self, configuration_set_name: str) -> None: """ Deletes a configuration set. :param configuration_set_name: The name of the configuration set to delete. """ try: self.sesv2_client.delete_configuration_set( ConfigurationSetName=configuration_set_name ) logger.info("Deleted configuration set '%s'.", configuration_set_name) except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": logger.warning( "Configuration set '%s' not found. Skipping deletion.", configuration_set_name, ) else: logger.error( "Error deleting configuration set '%s': %s", configuration_set_name, err.response["Error"]["Message"], ) raise def delete_email_identity(self, email_identity: str) -> None: """ Deletes an email identity. :param email_identity: The email address or domain to delete. """ try: self.sesv2_client.delete_email_identity(EmailIdentity=email_identity) logger.info("Deleted email identity '%s'.", email_identity) except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": logger.warning( "Email identity '%s' not found. Skipping deletion.", email_identity, ) else: logger.error( "Error deleting email identity '%s': %s", email_identity, err.response["Error"]["Message"], ) raise def delete_tenant(self, tenant_name: str) -> None: """ Deletes a tenant. :param tenant_name: The name of the tenant to delete. """ try: self.sesv2_client.delete_tenant(TenantName=tenant_name) logger.info("Deleted tenant '%s'.", tenant_name) except ClientError as err: if err.response["Error"]["Code"] == "NotFoundException": logger.warning( "Tenant '%s' not found. Skipping deletion.", tenant_name, ) else: logger.error( "Error deleting tenant '%s': %s", tenant_name, err.response["Error"]["Message"], ) raise def list_configuration_sets_page( self, filter_params: Dict[str, str], page_size: int, next_token: Optional[str] = None, ) -> Tuple[List[str], Optional[str]]: """ Gets one page of filtered configuration sets. Use this instead of list_configuration_sets to work with each page separately. :param filter_params: The filter to apply. :param page_size: The maximum number of results in the page. :param next_token: The NextToken from the previous page, or None for the first page. :return: The configuration set names in this page, and the NextToken for the next page, or None if this is the last page. """ kwargs = {"Filter": filter_params, "PageSize": page_size} if next_token is not None: kwargs["NextToken"] = next_token try: response = self.sesv2_client.list_configuration_sets(**kwargs) except ClientError as err: logger.error( "Error listing configuration sets: %s", err.response["Error"]["Message"], ) raise return response.get("ConfigurationSets", list()), response.get("NextToken")-
For API details, see the following topics in AWS SDK for Python (Boto3) API Reference.
-