From b5f02005f1bc148cd8bc77728639e1503890160e Mon Sep 17 00:00:00 2001 From: David Souther Date: Thu, 28 Mar 2024 09:50:50 -0400 Subject: [PATCH 01/13] Workflow (Java, Python, Rust): SESv2 Weekly Mailer Implementation in Java, Python, and Rust, with metadata, for SESv2 Weekly Mailer workflow. --- .doc_gen/metadata/sesv2_metadata.yaml | 259 +++++- .tools/readmes/config.py | 2 + javav2/example_code/ses/Readme.md | 45 +- javav2/example_code/ses/pom.xml | 17 +- .../ses/resources/config.properties | 4 + .../com/example/sesv2/NewsletterWorkflow.java | 391 ++++++++ .../example/sesv2/NewsletterWorkflowTest.java | 532 +++++++++++ python/example_code/sesv2/README.md | 88 ++ python/example_code/sesv2/newsletter.py | 312 +++++++ python/example_code/sesv2/newsletter_test.py | 335 +++++++ python/example_code/sesv2/requirements.txt | 2 + rustv1/examples/ses/Cargo.toml | 8 +- rustv1/examples/ses/README.md | 47 +- rustv1/examples/ses/src/bin/newsletter.rs | 45 + rustv1/examples/ses/src/lib.rs | 1 + rustv1/examples/ses/src/newsletter.rs | 410 +++++++++ rustv1/examples/ses/tests/test_newsletter.rs | 832 ++++++++++++++++++ workflows/sesv2_weekly_mailer/README.md | 9 +- .../sesv2_weekly_mailer/SPECIFICATION.md | 112 ++- .../{ => resources}/coupon-newsletter.html | 0 .../{ => resources}/coupon-newsletter.txt | 0 .../{ => resources}/sample_coupons.json | 0 .../{ => resources}/welcome.html | 0 .../{ => resources}/welcome.txt | 0 24 files changed, 3368 insertions(+), 83 deletions(-) create mode 100644 javav2/example_code/ses/resources/config.properties create mode 100644 javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java create mode 100644 javav2/example_code/ses/src/test/java/com/example/sesv2/NewsletterWorkflowTest.java create mode 100644 python/example_code/sesv2/README.md create mode 100644 python/example_code/sesv2/newsletter.py create mode 100644 python/example_code/sesv2/newsletter_test.py create mode 100644 python/example_code/sesv2/requirements.txt create mode 100644 rustv1/examples/ses/src/bin/newsletter.rs create mode 100644 rustv1/examples/ses/src/lib.rs create mode 100644 rustv1/examples/ses/src/newsletter.rs create mode 100644 rustv1/examples/ses/tests/test_newsletter.rs rename workflows/sesv2_weekly_mailer/{ => resources}/coupon-newsletter.html (100%) rename workflows/sesv2_weekly_mailer/{ => resources}/coupon-newsletter.txt (100%) rename workflows/sesv2_weekly_mailer/{ => resources}/sample_coupons.json (100%) rename workflows/sesv2_weekly_mailer/{ => resources}/welcome.html (100%) rename workflows/sesv2_weekly_mailer/{ => resources}/welcome.txt (100%) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 48b1df4814d..0a2a0790926 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -13,6 +13,22 @@ sesv2_CreateContactList: - description: snippet_tags: - ses.rust.create-contact-list + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/ses + excerpts: + - description: + snippet_tags: + - sesv2.java2.newsletter.CreateContactList + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.CreateContactList services: sesv2: {CreateContactList} sesv2_CreateContact: @@ -29,6 +45,22 @@ sesv2_CreateContact: - description: snippet_tags: - ses.rust.create-contact + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/ses + excerpts: + - description: + snippet_tags: + - sesv2.java2.newsletter.CreateContact + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.CreateContact services: sesv2: {CreateContact} sesv2_GetEmailIdentity: @@ -77,12 +109,28 @@ sesv2_ListContacts: - description: snippet_tags: - ses.rust.list-contacts + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/ses + excerpts: + - description: + snippet_tags: + - sesv2.java2.newsletter.ListContacts + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.ListContacts services: sesv2: {ListContacts} -sesv2_SendEmail: - title: Send an &SESv2; email using an &AWS; SDK - title_abbrev: Send an email - synopsis: send an &SESv2; email. +sesv2_SendEmail_Simple: + title: Send a simple &SESv2; email using an &AWS; SDK + title_abbrev: Send a simple email + synopsis: send a simple &SESv2; email. category: languages: Java: @@ -110,5 +158,208 @@ sesv2_SendEmail: - description: snippet_tags: - ruby.example_code.ses.v2.send_email + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SendEmail.simple services: sesv2: {SendEmail} +sesv2_SendEmail_Template: + title: Send a templated &SESv2; email using an &AWS; SDK + title_abbrev: Send a templated email + synopsis: send a templated &SESv2; email. + category: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/ses + excerpts: + - description: Sends a message. + snippet_tags: + - sesv2.java2.newsletter.SendEmail.template + Rust: + versions: + - sdk_version: 1 + github: rustv1/examples/ses + excerpts: + - description: Sends a message to all members of the contact list. + snippet_tags: + - sesv2.rust.send-email.template + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SendEmail.template + services: + sesv2: {SendEmail} +sesv2_CreateEmailIdentity: + title: Create an &SESv2; email identity using an &AWS; SDK + title_abbrev: Create an email identity + synopsis: create an &SESv2; email identity. + category: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/ses + excerpts: + - description: + snippet_tags: + - sesv2.java2.newsletter.CreateEmailIdentity + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.CreateEmailIdentity + Rust: + versions: + - sdk_version: 1 + github: rustv1/examples/ses + excerpts: + - description: + snippet_tags: + - sesv2.rust.create-email-identity + services: + sesv2: {CreateEmailIdentity} + +sesv2_CreateEmailTemplate: + title: Create an &SESv2; email template using an &AWS; SDK + title_abbrev: Create an email template + synopsis: create an &SESv2; email template. + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/ses + excerpts: + - description: + snippet_tags: + - sesv2.java2.newsletter.CreateEmailTemplate + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.CreateEmailTemplate + Rust: + versions: + - sdk_version: 1 + github: rustv1/examples/ses + excerpts: + - description: + snippet_tags: + - sesv2.rust.create-email-template + services: + sesv2: {CreateEmailTemplate} + +sesv2_DeleteContactList: + title: Delete an &SESv2; contact list using an &AWS; SDK + title_abbrev: Delete a contact list + synopsis: delete an &SESv2; contact list. + category: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/ses + excerpts: + - description: + snippet_tags: + - sesv2.java2.newsletter.DeleteContactList + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.DeleteContactList + Rust: + versions: + - sdk_version: 1 + github: rustv1/examples/ses + excerpts: + - description: + snippet_tags: + - sesv2.rust.delete-contact-list + services: + sesv2: {DeleteContactList} + +sesv2_DeleteEmailIdentity: + title: Delete an &SESv2; email identity using an &AWS; SDK + title_abbrev: Delete an email identity + synopsis: delete an &SESv2; email identity. + category: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/ses + excerpts: + - description: + snippet_tags: + - sesv2.java2.newsletter.DeleteEmailIdentity + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.DeleteEmailIdentity + Rust: + versions: + - sdk_version: 1 + github: rustv1/examples/ses + excerpts: + - description: + snippet_tags: + - sesv2.rust.delete-email-identity + services: + sesv2: {DeleteEmailIdentity} + +sesv2_DeleteEmailTemplate: + title: Delete an &SESv2; email template using an &AWS; SDK + title_abbrev: Delete an email template + synopsis: delete an &SESv2; email template. + category: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/ses + excerpts: + - description: + snippet_tags: + - sesv2.java2.newsletter.DeleteEmailTemplate + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.DeleteEmailTemplate + Rust: + versions: + - sdk_version: 1 + github: rustv1/examples/ses + excerpts: + - description: + snippet_tags: + - sesv2.rust.delete-email-template + services: + sesv2: {DeleteEmailTemplate} diff --git a/.tools/readmes/config.py b/.tools/readmes/config.py index 04b760f21d6..d34e726a5ab 100644 --- a/.tools/readmes/config.py +++ b/.tools/readmes/config.py @@ -44,6 +44,7 @@ "service_folder": 'javav2/example_code/{{service["name"]}}', "sdk_api_ref": 'https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/{{service["name"]}}/package-summary.html', "service_folder_overrides": { + "sesv2": "javav2/example_code/ses", "medical-imaging": "javav2/example_code/medicalimaging", }, }, @@ -139,6 +140,7 @@ "base_folder": "rustv1", "service_folder": 'rustv1/examples/{{service["name"]}}', "sdk_api_ref": 'https://docs.rs/aws-sdk-{{service["name"]}}/latest/aws_sdk_{{service["name"]}}/', + "service_folder_overrides": {"sesv2": "rustv1/examples/ses"}, } }, "SAP ABAP": { diff --git a/javav2/example_code/ses/Readme.md b/javav2/example_code/ses/Readme.md index 000c37ad718..392d37d5f73 100644 --- a/javav2/example_code/ses/Readme.md +++ b/javav2/example_code/ses/Readme.md @@ -1,20 +1,20 @@ -# Amazon SES code examples for the SDK for Java 2.x +# Amazon SES v2 API code examples for the SDK for Java 2.x ## Overview -Shows how to use the AWS SDK for Java 2.x to work with Amazon Simple Email Service (Amazon SES). +Shows how to use the AWS SDK for Java 2.x to work with Amazon Simple Email Service v2 API. -_Amazon SES is a reliable, scalable, and cost-effective email service._ +_Amazon SES v2 API is a reliable, scalable, and cost-effective email service._ ## ⚠ Important -* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). -* Running the tests might result in charges to your AWS account. -* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). -* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). +- Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +- Running the tests might result in charges to your AWS account. +- We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +- This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). @@ -25,7 +25,6 @@ _Amazon SES is a reliable, scalable, and cost-effective email service._ For prerequisites, see the [README](../../README.md#Prerequisites) in the `javav2` folder. - @@ -33,11 +32,16 @@ For prerequisites, see the [README](../../README.md#Prerequisites) in the `javav Code excerpts that show you how to call individual service functions. -- [List email templates](src/main/java/com/example/sesv2/ListTemplates.java#L6) (`ListTemplates`) -- [List identities](src/main/java/com/example/ses/ListIdentities.java#L6) (`ListIdentities`) -- [Send email](src/main/java/com/example/ses/SendMessageEmailRequest.java#L6) (`SendEmail`) -- [Send templated email](src/main/java/com/example/sesv2/SendEmailTemplate.java#L6) (`SendTemplatedEmail`) - +- [Create a contact in a contact list](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L199) (`CreateContact`) +- [Create a contact list](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L113) (`CreateContactList`) +- [Create an email identity](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L91) (`CreateEmailIdentity`) +- [Create an email template](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L133) (`CreateEmailTemplate`) +- [Delete a contact list](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L325) (`DeleteContactList`) +- [Delete an email identity](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L369) (`DeleteEmailIdentity`) +- [Delete an email template](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L344) (`DeleteEmailTemplate`) +- [List the contacts in a contact list](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L252) (`ListContacts`) +- [Send a simple email](src/main/java/com/example/sesv2/SendEmail.java#L6) (`SendEmail`) +- [Send a templated email](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L263) (`SendEmail`) @@ -46,30 +50,27 @@ Code excerpts that show you how to call individual service functions. ### Instructions - - +To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer/resources into a new folder, javav2/example_code/ses/resources/coupon_newsletter. + ### Tests ⚠ Running tests might result in charges to your AWS account. - To find instructions for running these tests, see the [README](../../README.md#Tests) in the `javav2` folder. - - ## Additional resources -- [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/Welcome.html) -- [Amazon SES API Reference](https://docs.aws.amazon.com/ses/latest/APIReference/Welcome.html) -- [SDK for Java 2.x Amazon SES reference](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/ses/package-summary.html) +- [Amazon SES v2 API Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/Welcome.html) +- [Amazon SES v2 API API Reference](https://docs.aws.amazon.com/ses/latest/APIReference-V2/Welcome.html) +- [SDK for Java 2.x Amazon SES v2 API reference](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/ses/package-summary.html) @@ -78,4 +79,4 @@ in the `javav2` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/javav2/example_code/ses/pom.xml b/javav2/example_code/ses/pom.xml index 73bc275c623..2517cd99b5c 100644 --- a/javav2/example_code/ses/pom.xml +++ b/javav2/example_code/ses/pom.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 AWSEmailJava2 AWSEmailJava2 @@ -77,14 +77,21 @@ 1.9.2 test + + org.hamcrest + hamcrest-all + 1.3 + test + + software.amazon.awssdk ses org.mockito - mockito-all - 1.10.19 + mockito-core + 5.11.0 test @@ -125,4 +132,4 @@ test - + \ No newline at end of file diff --git a/javav2/example_code/ses/resources/config.properties b/javav2/example_code/ses/resources/config.properties new file mode 100644 index 00000000000..11d39e183bd --- /dev/null +++ b/javav2/example_code/ses/resources/config.properties @@ -0,0 +1,4 @@ +sender = +recipient = +subject = +fileLocation = diff --git a/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java b/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java new file mode 100644 index 00000000000..8da8989820d --- /dev/null +++ b/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java @@ -0,0 +1,391 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.sesv2; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sesv2.SesV2Client; +import software.amazon.awssdk.services.sesv2.model.*; + +/** + * This class implements the SES v2 Coupon Newsletter Workflow. + * It demonstrates how to use the Amazon Simple Email Service (SES) v2 to send a + * coupon newsletter to a list of contacts. + */ +public class NewsletterWorkflow { + private static final String CONTACT_LIST_NAME = "weekly-coupons-newsletter"; + private static final String TEMPLATE_NAME = "weekly-coupons"; + private static final String INTRO = """ + Welcome to the Amazon SES v2 Coupon Newsletter Workflow! + + This workflow will help you: + 1. Prepare a verified email identity and contact list for your newsletter. + 2. Gather subscriber email addresses and send them a welcome email. + 3. Send a weekly coupon newsletter to your subscribers using email templates. + 4. Monitor your sending activity and metrics in the AWS console. + + Let's get started! + """; + private final SesV2Client sesClient; + private String verifiedEmail = ""; + + public void test_setVerifiedEmail(String verifiedEmail) { + this.verifiedEmail = verifiedEmail; + } + + /** + * Constructor for the Workflow class. + * + * @param sesClient The SesV2Client instance to be used for interacting with the + * SES v2 service. + */ + public NewsletterWorkflow(SesV2Client sesClient) { + this.sesClient = sesClient; + } + + /** + * The main entry point of the application. + * + * @param args Command-line arguments. + */ + public static void main(String[] args) { + System.out.println(INTRO); + SesV2Client sesClient = SesV2Client.builder() + .region(Region.AWS_GLOBAL) + .build(); + + new NewsletterWorkflow(sesClient).run(); + } + + /** + * Orchestrates the execution of the workflow steps. + */ + public void run() { + try { + prepareApplication(); + gatherSubscriberEmails(); + sendCouponNewsletter(); + monitorAndReview(); + } catch (Exception e) { + } + cleanUp(); + } + + /** + * Prepares the application by creating an email identity and a contact list. + */ + public void prepareApplication() throws IOException { + // 1. Create an email identity + System.out.println("Enter the verified email address: "); + Scanner scanner = new Scanner(System.in); + this.verifiedEmail = scanner.nextLine(); + scanner.close(); + + // snippet-start:[sesv2.java2.newsletter.CreateEmailIdentity] + try { + CreateEmailIdentityRequest createEmailIdentityRequest = CreateEmailIdentityRequest.builder() + .emailIdentity(verifiedEmail) + .build(); + sesClient.createEmailIdentity(createEmailIdentityRequest); + System.out.println("Email identity created: " + verifiedEmail); + } catch (AlreadyExistsException e) { + System.out.println("Email identity already exists, skipping creation: " + verifiedEmail); + } catch (NotFoundException e) { + System.err.println("The provided email address is not verified: " + verifiedEmail); + throw e; + } catch (LimitExceededException e) { + System.err + .println("You have reached the limit for email identities. Please remove some identities and try again."); + throw e; + } catch (SesV2Exception e) { + System.err.println("Error creating email identity: " + e.getMessage()); + throw e; + } + // snippet-end:[sesv2.java2.newsletter.CreateEmailIdentity] + + // snippet-start:[sesv2.java2.newsletter.CreateContactList] + try { + // 2. Create a contact list + String contactListName = CONTACT_LIST_NAME; + CreateContactListRequest createContactListRequest = CreateContactListRequest.builder() + .contactListName(contactListName) + .build(); + sesClient.createContactList(createContactListRequest); + System.out.println("Contact list created: " + contactListName); + } catch (AlreadyExistsException e) { + System.out.println("Contact list already exists, skipping creation: weekly-coupons-newsletter"); + } catch (LimitExceededException e) { + System.err.println("Limit for contact lists has been exceeded."); + throw e; + } catch (SesV2Exception e) { + System.err.println("Error creating contact list: " + e.getMessage()); + throw e; + } + // snippet-end:[sesv2.java2.newsletter.CreateContactList] + + // snippet-start:[sesv2.java2.newsletter.CreateEmailTemplate] + try { + // Create an email template named "weekly-coupons" + String newsletterHtml = Files.readString(Paths.get("resources/coupon_newsletter/coupon-newsletter.html")); + String newsletterText = Files.readString(Paths.get("resources/coupon_newsletter/coupon-newsletter.txt")); + + CreateEmailTemplateRequest templateRequest = CreateEmailTemplateRequest.builder() + .templateName(TEMPLATE_NAME) + .templateContent(EmailTemplateContent.builder() + .subject("Weekly Coupons Newsletter") + .html(newsletterHtml) + .text(newsletterText) + .build()) + .build(); + + sesClient.createEmailTemplate(templateRequest); + + System.out.println("Email template created: " + TEMPLATE_NAME); + } catch (AlreadyExistsException e) { + // If the template already exists, skip this step and proceed with the next + // operation + System.out.println("Email template already exists, skipping creation..."); + } catch (LimitExceededException e) { + // If the limit for email templates is exceeded, fail the workflow and inform + // the user + System.err.println("You have reached the limit for email templates. Please remove some templates and try again."); + throw e; + } catch (Exception e) { + System.err.println("Error occurred while creating email template: " + e.getMessage()); + throw e; + } + // snippet-end:[sesv2.java2.newsletter.CreateEmailTemplate] + } + + /** + * Helper method to create subscriber subaddresses. + * + * @param baseEmail The base email address (e.g., "user@example.com") + * @return A list of three email addresses with subaddress extensions + */ + private List createSubscriberSubaddresses(String baseEmail) { + List subaddresses = new ArrayList<>(); + String[] parts = baseEmail.split("@"); + String username = parts[0]; + String domain = parts[1]; + + for (int i = 1; i <= 3; i++) { + String subaddress = username + "+ses-weekly-newsletter-" + i + "@" + domain; + subaddresses.add(subaddress); + } + + return subaddresses; + } + + /** + * Gathers subscriber email addresses and sends a welcome email to each new + * subscriber. + */ + public void gatherSubscriberEmails() throws IOException { + Scanner scanner = new Scanner(System.in); + System.out.print("Enter a base email address for subscribing to the newsletter: "); + String baseEmail = scanner.nextLine(); + scanner.close(); + + for (String emailAddress : createSubscriberSubaddresses(baseEmail)) { + // "weekly-coupons-newsletter" contact list + // snippet-start:[sesv2.java2.newsletter.CreateContact] + try { + // Create a new contact with the provided email address in the + CreateContactRequest contactRequest = CreateContactRequest.builder() + .contactListName(CONTACT_LIST_NAME) + .emailAddress(emailAddress) + .build(); + + sesClient.createContact(contactRequest); + + System.out.println("Contact created: " + emailAddress); + + // snippet-start:[sesv2.java2.newsletter.SendEmail.simple] + // Send a welcome email to the new contact + String welcomeHtml = Files.readString(Paths.get("resources/coupon_newsletter/welcome.html")); + String welcomeText = Files.readString(Paths.get("resources/coupon_newsletter/welcome.txt")); + + SendEmailRequest welcomeEmailRequest = SendEmailRequest.builder() + .fromEmailAddress(this.verifiedEmail) + .destination(Destination.builder().toAddresses(emailAddress).build()) + .content(EmailContent.builder() + .simple( + Message.builder() + .subject(Content.builder().data("Welcome to the Weekly Coupons Newsletter").build()) + .body(Body.builder() + .text(Content.builder().data(welcomeText).build()) + .html(Content.builder().data(welcomeHtml).build()) + .build()) + .build()) + .build()) + .build(); + SendEmailResponse welcomeEmailResponse = sesClient.sendEmail(welcomeEmailRequest); + System.out.println("Welcome email sent: " + welcomeEmailResponse.messageId()); + // snippet-end:[sesv2.java2.newsletter.SendEmail.simple] + } catch (AlreadyExistsException e) { + // If the contact already exists, skip this step for that contact and proceed + // with the next contact + System.out.println("Contact already exists, skipping creation..."); + } catch (Exception e) { + System.err.println("Error occurred while processing email address " + emailAddress + ": " + e.getMessage()); + throw e; + } + } + // snippet-end:[sesv2.java2.newsletter.CreateContact] + } + + /** + * Sends the coupon newsletter to the list of contacts. + */ + public void sendCouponNewsletter() { + try { + // Retrieve the list of contacts from the "weekly-coupons-newsletter" contact + // list + // snippet-start:[sesv2.java2.newsletter.ListContacts] + ListContactsRequest contactListRequest = ListContactsRequest.builder() + .contactListName(CONTACT_LIST_NAME) + .build(); + ListContactsResponse contactListResponse = sesClient.listContacts(contactListRequest); + List contactEmails = contactListResponse.contacts().stream() + .map(Contact::emailAddress) + .toList(); + // snippet-end:[sesv2.java2.newsletter.ListContacts] + + // Send an email using the "weekly-coupons" template to each contact in the list + // snippet-start:[sesv2.java2.newsletter.SendEmail.template] + String coupons = Files.readString(Paths.get("resources/coupon_newsletter/sample_coupons.json")); + for (String emailAddress : contactEmails) { + SendEmailRequest newsletterRequest = SendEmailRequest.builder() + .destination(Destination.builder().toAddresses(emailAddress).build()) + .content(EmailContent.builder() + .template(Template.builder() + .templateName(TEMPLATE_NAME) + .templateData(coupons) + .build()) + .build()) + .fromEmailAddress(this.verifiedEmail) + .listManagementOptions(ListManagementOptions.builder() + .contactListName(CONTACT_LIST_NAME) + .build()) + .build(); + SendEmailResponse newsletterResponse = sesClient.sendEmail(newsletterRequest); + System.out.println("Newsletter sent to " + emailAddress + ": " + newsletterResponse.messageId()); + } + // snippet-end:[sesv2.java2.newsletter.SendEmail.template] + } catch (NotFoundException e) { + // If the contact list does not exist, fail the workflow and inform the user + System.err.println("The contact list is missing. Please create the contact list and try again."); + } catch (AccountSuspendedException e) { + // If the account is suspended, fail the workflow and inform the user + System.err.println("Your account is suspended. Please resolve the issue and try again."); + } catch (MailFromDomainNotVerifiedException e) { + // If the sending domain is not verified, fail the workflow and inform the user + System.err.println("The sending domain is not verified. Please verify your domain and try again."); + throw e; + } catch (MessageRejectedException e) { + // If the message is rejected due to invalid content, fail the workflow and + // inform the user + System.err.println("The message content is invalid. Please check your template and try again."); + throw e; + } catch (SendingPausedException e) { + // If sending is paused, fail the workflow and inform the user + System.err.println("Sending is currently paused for your account. Please resolve the issue and try again."); + throw e; + } catch (Exception e) { + System.err.println("Error occurred while sending the newsletter: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Monitors and reviews the newsletter campaign. + */ + public void monitorAndReview() { + System.out.println("\nMonitor your sending activity using the SES Homepage in the AWS console:\n" + + "https://console.aws.amazon.com/ses/home#/account\n" + + "For more detailed monitoring, refer to the SES Developer Guide:\n" + + "https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity.html"); + Scanner scanner = new Scanner(System.in); + scanner.nextLine(); + scanner.close(); + } + + /** + * Cleans up the resources created during the workflow. + */ + public void cleanUp() { + // snippet-start:[sesv2.java2.newsletter.DeleteContactList] + try { + // Delete the contact list + DeleteContactListRequest deleteContactListRequest = DeleteContactListRequest.builder() + .contactListName(CONTACT_LIST_NAME) + .build(); + + sesClient.deleteContactList(deleteContactListRequest); + + System.out.println("Contact list deleted: " + CONTACT_LIST_NAME); + } catch (NotFoundException e) { + // If the contact list does not exist, log the error and proceed + System.out.println("Contact list not found. Skipping deletion..."); + } catch (Exception e) { + System.err.println("Error occurred while deleting the contact list: " + e.getMessage()); + e.printStackTrace(); + } + // snippet-end:[sesv2.java2.newsletter.DeleteContactList] + + // snippet-start:[sesv2.java2.newsletter.DeleteEmailTemplate] + try { + // Delete the template + DeleteEmailTemplateRequest deleteTemplateRequest = DeleteEmailTemplateRequest.builder() + .templateName(TEMPLATE_NAME) + .build(); + + sesClient.deleteEmailTemplate(deleteTemplateRequest); + + System.out.println("Email template deleted: " + TEMPLATE_NAME); + } catch (NotFoundException e) { + // If the email template does not exist, log the error and proceed + System.out.println("Email template not found. Skipping deletion..."); + } catch (Exception e) { + System.err.println("Error occurred while deleting the email template: " + e.getMessage()); + e.printStackTrace(); + } + // snippet-end:[sesv2.java2.newsletter.DeleteEmailTemplate] + + System.out.println("\nDo you want to delete the email identity? (y/n)"); + Scanner scanner = new Scanner(System.in); + String input = scanner.nextLine(); + scanner.close(); + + if (input.equalsIgnoreCase("y")) { + // snippet-start:[sesv2.java2.newsletter.DeleteEmailIdentity] + try { + // Delete the email identity + DeleteEmailIdentityRequest deleteIdentityRequest = DeleteEmailIdentityRequest.builder() + .emailIdentity(this.verifiedEmail) + .build(); + + sesClient.deleteEmailIdentity(deleteIdentityRequest); + + System.out.println("Email identity deleted: " + this.verifiedEmail); + } catch (NotFoundException e) { + // If the email identity does not exist, log the error and proceed + System.out.println("Email identity not found. Skipping deletion..."); + } catch (Exception e) { + System.err.println("Error occurred while deleting the email identity: " + e.getMessage()); + e.printStackTrace(); + } + } else { + System.out.println("Skipping email identity deletion."); + } + // snippet-end:[sesv2.java2.newsletter.DeleteEmailIdentity] + } +} diff --git a/javav2/example_code/ses/src/test/java/com/example/sesv2/NewsletterWorkflowTest.java b/javav2/example_code/ses/src/test/java/com/example/sesv2/NewsletterWorkflowTest.java new file mode 100644 index 00000000000..4b6f28f7968 --- /dev/null +++ b/javav2/example_code/ses/src/test/java/com/example/sesv2/NewsletterWorkflowTest.java @@ -0,0 +1,532 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.sesv2; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import software.amazon.awssdk.services.sesv2.SesV2Client; +import software.amazon.awssdk.services.sesv2.model.*; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.Mockito.*; + +public class NewsletterWorkflowTest { + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private NewsletterWorkflow scenario; + + private AutoCloseable closeable; + + @Mock + private SesV2Client sesClient; + + @Before + public void openMocks() { + closeable = MockitoAnnotations.openMocks(this); + scenario = new NewsletterWorkflow(sesClient); + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + outContent.reset(); + errContent.reset(); + } + + @After + public void releaseMocks() throws Exception { + closeable.close(); + } + + // Prepare Application Tests + @Test + public void test_prepareApplication_success() throws IOException { + // Mock the necessary AWS SDK calls and responses + String verifiedEmail = "test@example.com"; + CreateEmailIdentityResponse emailIdentityResponse = CreateEmailIdentityResponse.builder().build(); + when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenReturn(emailIdentityResponse); + + CreateContactListResponse contactListResponse = CreateContactListResponse.builder().build(); + when(sesClient.createContactList(any(CreateContactListRequest.class))).thenReturn(contactListResponse); + + CreateEmailTemplateResponse createEmailTemplateResponse = CreateEmailTemplateResponse.builder().build(); + when(sesClient.createEmailTemplate(any(CreateEmailTemplateRequest.class))).thenReturn(createEmailTemplateResponse); + + System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); + + scenario.prepareApplication(); + + assertThat(outContent.toString(), containsString("Email identity created: test@example.com")); + assertThat(outContent.toString(), containsString("Contact list created: weekly-coupons-newsletter")); + } + + @Test + public void test_prepareApplication_error_identityAlreadyExists() { + // Mock the necessary AWS SDK calls and responses + String verifiedEmail = "test@example.com"; + when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenThrow(AlreadyExistsException.class); + + CreateContactListResponse contactListResponse = CreateContactListResponse.builder().build(); + when(sesClient.createContactList(any(CreateContactListRequest.class))).thenReturn(contactListResponse); + + System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); + + try { + scenario.prepareApplication(); + } catch (Exception e) { + } + + assertThat(outContent.toString(), + containsString("Email identity already exists, skipping creation: test@example.com")); + assertThat(outContent.toString(), containsString("Contact list created: weekly-coupons-newsletter")); + } + + @Test + public void test_prepareApplication_error_identityNotFound() { + // Mock the necessary AWS SDK calls and responses + String verifiedEmail = "test@example.com"; + when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenThrow(NotFoundException.class); + + System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); + + try { + scenario.prepareApplication(); + } catch (Exception e) { + } + + assertThat(errContent.toString(), containsString("The provided email address is not verified: test@example.com")); + } + + @Test + public void test_prepareApplication_error_identityLimitExceeded() { + // Mock the necessary AWS SDK calls and responses + String verifiedEmail = "test@example.com"; + when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenThrow(LimitExceededException.class); + + System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); + + try { + scenario.prepareApplication(); + } catch (Exception e) { + } + + assertThat(errContent.toString(), containsString( + "You have reached the limit for email identities. Please remove some identities and try again.")); + } + + @Test + public void test_prepareApplication_error_contactListLimitExceeded() { + // Mock the necessary AWS SDK calls and responses + String verifiedEmail = "test@example.com"; + CreateEmailIdentityResponse emailIdentityResponse = CreateEmailIdentityResponse.builder().build(); + when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenReturn(emailIdentityResponse); + + when(sesClient.createContactList(any(CreateContactListRequest.class))).thenThrow(LimitExceededException.class); + + System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); + + try { + scenario.prepareApplication(); + } catch (Exception e) { + } + + assertThat(outContent.toString(), containsString("Email identity created: test@example.com")); + assertThat(errContent.toString(), containsString("Limit for contact lists has been exceeded.")); + } + + @Test + public void test_prepareApplication_error_templateAlreadyExists() { + // Mock the necessary AWS SDK calls and responses + String verifiedEmail = "test@example.com"; + CreateEmailIdentityResponse emailIdentityResponse = CreateEmailIdentityResponse.builder().build(); + when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenReturn(emailIdentityResponse); + + CreateContactListResponse contactListResponse = CreateContactListResponse.builder().build(); + when(sesClient.createContactList(any(CreateContactListRequest.class))).thenReturn(contactListResponse); + + when(sesClient.createEmailTemplate(any(CreateEmailTemplateRequest.class))).thenThrow(AlreadyExistsException.class); + + System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); + + try { + scenario.prepareApplication(); + } catch (Exception e) { + } + + String output = outContent.toString(); + assertThat(output, containsString("Email template already exists, skipping creation...")); + } + + @Test + public void test_prepareApplication_error_templateLimitExceeded() { + // Mock the necessary AWS SDK calls and responses + // Mock the necessary AWS SDK calls and responses + String verifiedEmail = "test@example.com"; + System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); + + CreateEmailIdentityResponse emailIdentityResponse = CreateEmailIdentityResponse.builder().build(); + when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenReturn(emailIdentityResponse); + + CreateContactListResponse contactListResponse = CreateContactListResponse.builder().build(); + when(sesClient.createContactList(any(CreateContactListRequest.class))).thenReturn(contactListResponse); + + when(sesClient.createEmailTemplate(any(CreateEmailTemplateRequest.class))).thenThrow(LimitExceededException.class); + + try { + scenario.prepareApplication(); + } catch (Exception e) { + } + + String errorOutput = errContent.toString(); + assertThat(errorOutput, + containsString("You have reached the limit for email templates. Please remove some templates and try again.")); + } + + // Gather Subscriber Emails Tests + + @Test + public void test_gatherSubscriberEmails_success() throws IOException { + // Mock the necessary AWS SDK calls and responses + String baseEmail = "user@example.com"; + CreateContactResponse contactResponse = CreateContactResponse.builder().build(); + when(sesClient.createContact(any(CreateContactRequest.class))).thenReturn(contactResponse); + + SendEmailResponse welcomeEmailResponse = SendEmailResponse.builder().messageId("message-id").build(); + when(sesClient.sendEmail(any(SendEmailRequest.class))).thenReturn(welcomeEmailResponse); + + System.setIn(new ByteArrayInputStream(baseEmail.getBytes())); + + scenario.gatherSubscriberEmails(); + + String output = outContent.toString(); + for (int i = 1; i <= 3; i++) { + String expectedEmail = baseEmail + "+ses-weekly-newsletter-" + i + + "@example.com"; + assertThat(output, containsString("Contact created: " + expectedEmail)); + assertThat(output, containsString("Welcome email sent: message-id")); + } + } + + @Test + public void test_gatherSubscriberEmails_error_contactAlreadyExists() { + // Mock the necessary AWS SDK calls and responses + String baseEmail = "user@example.com"; + when(sesClient.createContact(any(CreateContactRequest.class))).thenThrow( + AlreadyExistsException.class); + + SendEmailResponse welcomeEmailResponse = SendEmailResponse.builder().messageId("message-id").build(); + when(sesClient.sendEmail(any(SendEmailRequest.class))).thenReturn( + welcomeEmailResponse); + + System.setIn(new ByteArrayInputStream(baseEmail.getBytes())); + + try { + scenario.gatherSubscriberEmails(); + } catch (Exception e) { + } + + String output = outContent.toString(); + assertThat(output, containsString("Contact already exists, skipping creation...")); + assertThat(output, containsString("Contact already exists, skipping creation...")); + assertThat(output, containsString("Welcome email sent: message-id")); + } + + @Test + public void test_gatherSubscriberEmails_error_sendEmailFailed() { + // Mock the necessary AWS SDK calls and responses + String baseEmail = "user@example.com"; + CreateContactResponse contactResponse = CreateContactResponse.builder().build(); + when(sesClient.createContact(any(CreateContactRequest.class))).thenReturn( + contactResponse); + + when(sesClient.sendEmail(any(SendEmailRequest.class))).thenThrow( + SesV2Exception.class); + + System.setIn(new ByteArrayInputStream(baseEmail.getBytes())); + + try { + scenario.gatherSubscriberEmails(); + } catch (Exception e) { + } + + String errorOutput = errContent.toString(); + String expectedEmail = "user+ses-weekly-newsletter-1@example.com"; + assertThat(errorOutput, containsString("Error occurred while processing email address " + expectedEmail)); + } + + // Send Coupon Newsletter Tests + + @Test + public void test_sendCouponNewsletter_success() { + ListContactsResponse contactListResponse = ListContactsResponse.builder() + .contacts( + Contact.builder().emailAddress("user+ses-weekly-newsletter-1@example.com").build(), + Contact.builder().emailAddress("user+ses-weekly-newsletter-2@example.com").build(), + Contact.builder().emailAddress("user+ses-weekly-newsletter-3@example.com").build()) + .build(); + when(sesClient.listContacts(any(ListContactsRequest.class))).thenReturn( + contactListResponse); + + SendEmailResponse newsletterResponse = SendEmailResponse.builder().messageId("message-id").build(); + when(sesClient.sendEmail(any(SendEmailRequest.class))).thenReturn( + newsletterResponse); + + scenario.sendCouponNewsletter(); + + String output = outContent.toString(); + for (int i = 1; i <= 3; i++) { + + assertThat(output, + containsString("Newsletter sent to user+ses-weekly-newsletter-" + i + "@example.com: message-id")); + } + } + + @Test + public void test_sendCouponNewsletter_error_contactListNotFound() { + // Mock the necessary AWS SDK calls and responses + CreateEmailTemplateResponse templateResponse = CreateEmailTemplateResponse.builder().build(); + when(sesClient.createEmailTemplate(any(CreateEmailTemplateRequest.class))).thenReturn(templateResponse); + + when(sesClient.listContacts(any(ListContactsRequest.class))).thenThrow( + NotFoundException.class); + + try { + scenario.sendCouponNewsletter(); + } catch (Exception e) { + } + + String errorOutput = errContent.toString(); + assertThat(errorOutput, + containsString("The contact list is missing. Please create the contact list and try again.")); + } + + @Test + public void test_sendCouponNewsletter_error_accountSuspended() { + // Mock the necessary AWS SDK calls and responses + CreateEmailTemplateResponse templateResponse = CreateEmailTemplateResponse.builder().build(); + when(sesClient.createEmailTemplate(any(CreateEmailTemplateRequest.class))).thenReturn(templateResponse); + + ListContactsResponse contactListResponse = ListContactsResponse.builder() + .contacts(Contact.builder().emailAddress("user@example.com").build()) + .build(); + when(sesClient.listContacts(any(ListContactsRequest.class))).thenReturn( + contactListResponse); + + when(sesClient.sendEmail(any(SendEmailRequest.class))).thenThrow( + AccountSuspendedException.class); + + try { + scenario.sendCouponNewsletter(); + } catch (Exception e) { + } + + String errorOutput = errContent.toString(); + assertThat(errorOutput, containsString("Your account is suspended. Please resolve the issue and try again.")); + } + + @Test + public void test_sendCouponNewsletter_error_domainNotVerified() { + // Mock the necessary AWS SDK calls and responses + CreateEmailTemplateResponse templateResponse = CreateEmailTemplateResponse.builder().build(); + when(sesClient.createEmailTemplate(any(CreateEmailTemplateRequest.class))).thenReturn(templateResponse); + + ListContactsResponse contactListResponse = ListContactsResponse.builder() + .contacts(Contact.builder().emailAddress("user@example.com").build()) + .build(); + when(sesClient.listContacts(any(ListContactsRequest.class))).thenReturn( + contactListResponse); + + when(sesClient.sendEmail(any(SendEmailRequest.class))).thenThrow( + MailFromDomainNotVerifiedException.class); + + try { + scenario.sendCouponNewsletter(); + } catch (Exception e) { + } + + String errorOutput = errContent.toString(); + assertThat(errorOutput, + containsString("The sending domain is not verified. Please verify your domain and try again.")); + } + + @Test + public void test_sendCouponNewsletter_error_messageRejected() { + // Mock the necessary AWS SDK calls and responses + CreateEmailTemplateResponse templateResponse = CreateEmailTemplateResponse.builder().build(); + when(sesClient.createEmailTemplate(any(CreateEmailTemplateRequest.class))).thenReturn(templateResponse); + + ListContactsResponse contactListResponse = ListContactsResponse.builder() + .contacts(Contact.builder().emailAddress("user@example.com").build()) + .build(); + when(sesClient.listContacts(any(ListContactsRequest.class))).thenReturn( + contactListResponse); + + when(sesClient.sendEmail(any(SendEmailRequest.class))).thenThrow( + MessageRejectedException.class); + + try { + scenario.sendCouponNewsletter(); + } catch (Exception e) { + } + + String errorOutput = errContent.toString(); + assertThat(errorOutput, + containsString("The message content is invalid. Please check your template and try again.")); + } + + @Test + public void test_sendCouponNewsletter_error_sendingPaused() { + // Mock the necessary AWS SDK calls and responses + CreateEmailTemplateResponse templateResponse = CreateEmailTemplateResponse.builder().build(); + when(sesClient.createEmailTemplate(any(CreateEmailTemplateRequest.class))).thenReturn(templateResponse); + + ListContactsResponse contactListResponse = ListContactsResponse.builder() + .contacts(Contact.builder().emailAddress("user@example.com").build()) + .build(); + when(sesClient.listContacts(any(ListContactsRequest.class))).thenReturn( + contactListResponse); + + when(sesClient.sendEmail(any(SendEmailRequest.class))).thenThrow( + SendingPausedException.class); + + try { + scenario.sendCouponNewsletter(); + } catch (Exception e) { + } + + String errorOutput = errContent.toString(); + assertThat( + errorOutput, + containsString("Sending is currently paused for your account. Please resolve the issue and try again.")); + } + + // Clean Up Tests + + @Test + public void test_cleanUp_success() { + // Mock the necessary AWS SDK calls and responses + scenario.test_setVerifiedEmail("test@example.com"); + DeleteContactListResponse deleteContactListResponse = DeleteContactListResponse.builder().build(); + when(sesClient.deleteContactList(any(DeleteContactListRequest.class))).thenReturn(deleteContactListResponse); + + DeleteEmailTemplateResponse deleteTemplateResponse = DeleteEmailTemplateResponse.builder().build(); + when(sesClient.deleteEmailTemplate(any(DeleteEmailTemplateRequest.class))).thenReturn(deleteTemplateResponse); + + DeleteEmailIdentityResponse deleteIdentityResponse = DeleteEmailIdentityResponse.builder().build(); + when(sesClient.deleteEmailIdentity(any(DeleteEmailIdentityRequest.class))).thenReturn(deleteIdentityResponse); + + System.setIn(new ByteArrayInputStream("y".getBytes())); + + scenario.cleanUp(); + + String output = outContent.toString(); + assertThat(output, containsString("Contact list deleted: weekly-coupons-newsletter")); + assertThat(output, containsString("Email template deleted: weekly-coupons")); + assertThat(output, containsString("Email identity deleted: test@example.com")); + } + + @Test + public void test_cleanUp_error_contactListNotFound() { + // Mock the necessary AWS SDK calls and responses + scenario.test_setVerifiedEmail("test@example.com"); + when(sesClient.deleteContactList(any(DeleteContactListRequest.class))).thenThrow(NotFoundException.class); + + DeleteEmailTemplateResponse deleteTemplateResponse = DeleteEmailTemplateResponse.builder().build(); + when(sesClient.deleteEmailTemplate(any(DeleteEmailTemplateRequest.class))).thenReturn(deleteTemplateResponse); + + DeleteEmailIdentityResponse deleteIdentityResponse = DeleteEmailIdentityResponse.builder().build(); + when(sesClient.deleteEmailIdentity(any(DeleteEmailIdentityRequest.class))).thenReturn(deleteIdentityResponse); + + System.setIn(new ByteArrayInputStream("y".getBytes())); + + try { + scenario.cleanUp(); + } catch (Exception e) { + } + + String output = outContent.toString(); + assertThat(output, containsString("Contact list not found. Skipping deletion...")); + assertThat(output, containsString("Email template deleted: weekly-coupons")); + assertThat(output, containsString("Email identity deleted: test@example.com")); + } + + @Test + public void test_cleanUp_error_templateNotFound() { + // Mock the necessary AWS SDK calls and responses + scenario.test_setVerifiedEmail("test@example.com"); + DeleteContactListResponse deleteContactListResponse = DeleteContactListResponse.builder().build(); + when(sesClient.deleteContactList(any(DeleteContactListRequest.class))).thenReturn(deleteContactListResponse); + + when(sesClient.deleteEmailTemplate(any(DeleteEmailTemplateRequest.class))).thenThrow(NotFoundException.class); + + DeleteEmailIdentityResponse deleteIdentityResponse = DeleteEmailIdentityResponse.builder().build(); + when(sesClient.deleteEmailIdentity(any(DeleteEmailIdentityRequest.class))).thenReturn(deleteIdentityResponse); + + System.setIn(new ByteArrayInputStream("y".getBytes())); + + try { + scenario.cleanUp(); + } catch (Exception e) { + } + + String output = outContent.toString(); + assertThat(output, containsString("Contact list deleted: weekly-coupons-newsletter")); + assertThat(output, containsString("Email template not found. Skipping deletion...")); + assertThat(output, containsString("Email identity deleted: test@example.com")); + } + + @Test + public void test_cleanUp_error_identityNotFound() { + // Mock the necessary AWS SDK calls and responses + scenario.test_setVerifiedEmail("test@example.com"); + DeleteContactListResponse deleteContactListResponse = DeleteContactListResponse.builder().build(); + when(sesClient.deleteContactList(any(DeleteContactListRequest.class))).thenReturn(deleteContactListResponse); + + DeleteEmailTemplateResponse deleteTemplateResponse = DeleteEmailTemplateResponse.builder().build(); + when(sesClient.deleteEmailTemplate(any(DeleteEmailTemplateRequest.class))).thenReturn(deleteTemplateResponse); + + when(sesClient.deleteEmailIdentity(any(DeleteEmailIdentityRequest.class))).thenThrow(NotFoundException.class); + + System.setIn(new ByteArrayInputStream("y".getBytes())); + + try { + scenario.cleanUp(); + } catch (Exception e) { + } + + String output = outContent.toString(); + assertThat(output, containsString("Contact list deleted: weekly-coupons-newsletter")); + assertThat(output, containsString("Email template deleted: weekly-coupons")); + assertThat(output, containsString("Email identity not found. Skipping deletion...")); + } + + @Test + public void test_cleanUp_skipIdentityDeletion() { + // Mock the necessary AWS SDK calls and responses + scenario.test_setVerifiedEmail("test@example.com"); + DeleteContactListResponse deleteContactListResponse = DeleteContactListResponse.builder().build(); + when(sesClient.deleteContactList(any(DeleteContactListRequest.class))).thenReturn(deleteContactListResponse); + + DeleteEmailTemplateResponse deleteTemplateResponse = DeleteEmailTemplateResponse.builder().build(); + when(sesClient.deleteEmailTemplate(any(DeleteEmailTemplateRequest.class))).thenReturn(deleteTemplateResponse); + + System.setIn(new ByteArrayInputStream("n".getBytes())); + + try { + scenario.cleanUp(); + } catch (Exception e) { + } + + String output = outContent.toString(); + assertThat(output, containsString("Contact list deleted: weekly-coupons-newsletter")); + assertThat(output, containsString("Email template deleted: weekly-coupons")); + assertThat(output, containsString("Skipping email identity deletion.")); + } +} diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md new file mode 100644 index 00000000000..603b815c4ec --- /dev/null +++ b/python/example_code/sesv2/README.md @@ -0,0 +1,88 @@ +# Amazon SES v2 API code examples for the SDK for Python + +## Overview + +Shows how to use the AWS SDK for Python (Boto3) to work with Amazon Simple Email Service v2 API. + + + + +_Amazon SES v2 API is a reliable, scalable, and cost-effective email service._ + +## ⚠ Important + +- Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +- Running the tests might result in charges to your AWS account. +- We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +- This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../README.md#Prerequisites) in the `python` folder. + +Install the packages required by these examples by running the following in a virtual environment: + +``` +python -m pip install -r requirements.txt +``` + + + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [Create a contact in a contact list](newsletter.py#L149) (`CreateContact`) +- [Create a contact list](newsletter.py#L99) (`CreateContactList`) +- [Create an email identity](newsletter.py#L86) (`CreateEmailIdentity`) +- [Create an email template](newsletter.py#L112) (`CreateEmailTemplate`) +- [Delete a contact list](newsletter.py#L250) (`DeleteContactList`) +- [Delete an email identity](newsletter.py#L278) (`DeleteEmailIdentity`) +- [Delete an email template](newsletter.py#L263) (`DeleteEmailTemplate`) +- [List the contacts in a contact list](newsletter.py#L192) (`ListContacts`) +- [Send a simple email](newsletter.py#L158) (`SendEmail`) +- [Send a templated email](newsletter.py#L211) (`SendEmail`) + + + + +## Run the examples + +### Instructions + + + +To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer/resources into this folder. + + + +### Tests + +⚠ Running tests might result in charges to your AWS account. + +To find instructions for running these tests, see the [README](../../README.md#Tests) +in the `python` folder. + + + + +## Additional resources + +- [Amazon SES v2 API Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/Welcome.html) +- [Amazon SES v2 API API Reference](https://docs.aws.amazon.com/ses/latest/APIReference-V2/Welcome.html) +- [SDK for Python Amazon SES v2 API reference](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sesv2.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/python/example_code/sesv2/newsletter.py b/python/example_code/sesv2/newsletter.py new file mode 100644 index 00000000000..e116b17cfe1 --- /dev/null +++ b/python/example_code/sesv2/newsletter.py @@ -0,0 +1,312 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import boto3 +from botocore.exceptions import ClientError +from time import sleep + +# Constants +CONTACT_LIST_NAME = "weekly-coupons-newsletter" +TEMPLATE_NAME = "weekly-coupons" + +INTRO = """ +Welcome to the Amazon SES v2 Coupon Newsletter Workflow! + +This workflow will help you: +1. Prepare a verified email identity and contact list for your newsletter. +2. Gather subscriber email addresses and send them a welcome email. +3. Send a weekly coupon newsletter to your subscribers using email templates. +4. Monitor your sending activity and metrics in the AWS console. + +Let's get started! +""" + + +# Helper functions +def load_file_content(file_path): + """ + Loads the content of a file. + + Args: + file_path (str): The path to the file. + + Returns: + str: The content of the file. + """ + with open(file_path, "r") as file: + content = file.read() + return content + + +def print_error(error): + """ + Prints the error message to the console. + + Args: + error (Exception): The exception object. + """ + print(f"Error: {error}") + + +def get_subaddress_variants(base_email, num_variants): + """ + Generates subaddress variants of a base email address. + + Args: + base_email (str): The base email address. + num_variants (int): The number of variants to generate. + + Returns: + list: A list of subaddress variants. + """ + user_part, domain_part = base_email.split("@") + variants = [ + f"{user_part}+ses-weekly-newsletter-{i}@{domain_part}" + for i in range(1, num_variants + 1) + ] + return variants + + +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 + + def prepare_application(self): + """ + Prepares the application by creating an email identity and a contact list. + """ + # Get the verified email address from the user + self.verified_email = input("Enter the verified email address: ") + + # Create the email identity + # snippet-start:[python.example_code.sesv2.CreateEmailIdentity] + 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 + # snippet-end:[python.example_code.sesv2.CreateEmailIdentity] + + # Create the contact list + # snippet-start:[python.example_code.sesv2.CreateContactList] + 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 + # snippet-end:[python.example_code.sesv2.CreateContactList] + + # Create the email template + # snippet-start:[python.example_code.sesv2.CreateEmailTemplate] + 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 + # snippet-end:[python.example_code.sesv2.CreateEmailTemplate] + + def gather_subscriber_email_addresses(self): + """ + Gathers subscriber email addresses and sends a welcome email to each subscriber. + """ + # Get the base email address from the user + base_email = input( + "Enter a base email address for subscribing to the newsletter: " + ) + + # Generate subaddress variants + email_variants = get_subaddress_variants(base_email, 3) + + # Load the welcome email content + welcome_text = load_file_content("welcome.txt") + welcome_html = load_file_content("welcome.html") + + # Create contacts and send welcome emails + for email in email_variants: + # snippet-start:[python.example_code.sesv2.CreateContact] + 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 + # snippet-start:[python.example_code.sesv2.SendEmail.simple] + 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}'.") + # snippet-end:[python.example_code.sesv2.SendEmail.simple] + 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 + # snippet-end:[python.example_code.sesv2.CreateContact] + + def send_coupon_newsletter(self): + """ + Sends the coupon newsletter to the subscribers. + """ + # Get the list of contacts + # snippet-start:[python.example_code.sesv2.ListContacts] + 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 + # snippet-end:[python.example_code.sesv2.ListContacts] + + # Send the coupon newsletter to each contact + coupon_items = load_file_content("sample_coupons.json") + + for contact in contacts_response["Contacts"]: + email_address = contact["EmailAddress"] + try: + # snippet-start:[python.example_code.sesv2.SendEmail.template] + send = 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}, + ) + # snippet-end:[python.example_code.sesv2.SendEmail.template] + print(f"Newsletter sent to '{email_address}'.") + print("Debug: ", send) + if self.sleep: + # 1 email per second in sandbox mode, remove in production. + sleep(1.1) + except ClientError as e: + print_error(e) + + def monitor_and_review(self): + """ + Provides instructions for monitoring sending activity in the AWS console. + """ + print( + "To monitor your sending activity, please visit the SES Homepage in the AWS console:" + ) + print("https://console.aws.amazon.com/ses/home#/account") + print( + "From there, you can view various dashboards and metrics related to your newsletter campaign." + ) + input("Press enter to continue.") + + def clean_up(self): + """ + Cleans up the resources created during the workflow. + """ + # Delete the contact list + # snippet-start:[python.example_code.sesv2.DeleteContactList] + 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) + # snippet-end:[python.example_code.sesv2.DeleteContactList] + + # Delete the email template + # snippet-start:[python.example_code.sesv2.DeleteEmailTemplate] + 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) + # snippet-end:[python.example_code.sesv2.DeleteEmailTemplate] + + # Ask the user if they want to delete the email identity + delete_identity = input("Do you want to delete the email identity? (y/n) ") + if delete_identity.lower() == "y": + # snippet-start:[python.example_code.sesv2.DeleteEmailIdentity] + 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) + # snippet-end:[python.example_code.sesv2.DeleteEmailIdentity] + else: + print("Skipping email identity deletion.") + + +# Main function +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() + + +if __name__ == "__main__": + main() diff --git a/python/example_code/sesv2/newsletter_test.py b/python/example_code/sesv2/newsletter_test.py new file mode 100644 index 00000000000..7ed9c47c703 --- /dev/null +++ b/python/example_code/sesv2/newsletter_test.py @@ -0,0 +1,335 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json +import unittest +import sys +from botocore.exceptions import ClientError +from io import StringIO +from unittest.mock import patch + +from python.example_code.sesv2.newsletter import ( + SESv2Workflow, + get_subaddress_variants, + CONTACT_LIST_NAME, + TEMPLATE_NAME, +) + +# Run tests with `python -m unittest` + + +class TestSESv2WorkflowPrepareApplication(unittest.TestCase): + @patch("main.boto3.client") + def setUp(self, mock_client): + self.ses_client = mock_client() + self.workflow = SESv2Workflow(self.ses_client) + + # Tests for prepare_application + @patch("builtins.input", return_value="verified@example.com") + def test_prepare_application_success(self, mock_input): + self.ses_client.create_email_identity.return_value = None + self.ses_client.create_contact_list.return_value = None + + captured_output = StringIO() + sys.stdout = captured_output + self.workflow.prepare_application() + sys.stdout = sys.__stdout__ # Reset standard output + + expected_output = "Email identity 'verified@example.com' created successfully.\nContact list 'weekly-coupons-newsletter' created successfully.\nEmail template 'weekly-coupons' created successfully.\n" + self.assertEqual(captured_output.getvalue(), expected_output) + + @patch("builtins.input", return_value="verified@example.com") + def test_prepare_application_error_identity_already_exists(self, mock_input): + self.ses_client.create_email_identity.side_effect = ClientError( + error_response={"Error": {"Code": "AlreadyExistsException"}}, + operation_name="CreateEmailIdentity", + ) + self.ses_client.create_contact_list.return_value = None + + captured_output = StringIO() + sys.stdout = captured_output + self.workflow.prepare_application() + sys.stdout = sys.__stdout__ + + expected_output = "Email identity 'verified@example.com' already exists.\nContact list 'weekly-coupons-newsletter' created successfully.\nEmail template 'weekly-coupons' created successfully.\n" + self.assertEqual(captured_output.getvalue(), expected_output) + + @patch("builtins.input", return_value="invalid@example.com") + def test_prepare_application_error_identity_not_found(self, mock_input): + self.ses_client.create_email_identity.side_effect = ClientError( + error_response={"Error": {"Code": "NotFoundException"}}, + operation_name="CreateEmailIdentity", + ) + self.ses_client.create_contact_list.return_value = None + + captured_output = StringIO() + sys.stdout = captured_output + with self.assertRaises(ClientError): + self.workflow.prepare_application() + sys.stdout = sys.__stdout__ + + expected_output = "" + self.assertEqual(captured_output.getvalue(), expected_output) + + @patch("builtins.input", return_value="verified@example.com") + def test_prepare_application_error_identity_limit_exceeded(self, mock_input): + self.ses_client.create_email_identity.side_effect = ClientError( + error_response={"Error": {"Code": "LimitExceededException"}}, + operation_name="CreateEmailIdentity", + ) + self.ses_client.create_contact_list.return_value = None + + captured_output = StringIO() + sys.stdout = captured_output + with self.assertRaises(ClientError): + self.workflow.prepare_application() + sys.stdout = sys.__stdout__ + + expected_output = "" + self.assertEqual(captured_output.getvalue(), expected_output) + + @patch("builtins.input", return_value="verified@example.com") + def test_prepare_application_error_contact_list_limit_exceeded(self, mock_input): + self.ses_client.create_email_identity.return_value = None + self.ses_client.create_contact_list.side_effect = ClientError( + error_response={"Error": {"Code": "LimitExceededException"}}, + operation_name="CreateContactList", + ) + + captured_output = StringIO() + sys.stdout = captured_output + with self.assertRaises(ClientError): + self.workflow.prepare_application() + sys.stdout = sys.__stdout__ + + expected_output = ( + "Email identity 'verified@example.com' created successfully.\n" + ) + self.assertEqual(captured_output.getvalue(), expected_output) + + +class TestSESv2WorkflowGatherSubscribers(unittest.TestCase): + @patch("main.boto3.client") + def setUp(self, mock_client): + self.ses_client = mock_client() + self.workflow = SESv2Workflow(self.ses_client, sleep=False) + self.workflow.verified_email = "verified@example.com" + + # Tests for gather_subscriber_email_addresses + @patch("builtins.input", return_value="user@example.com") + def test_gather_subscriber_email_addresses_success(self, mock_input): + self.ses_client.create_contact.return_value = None + self.ses_client.send_email.return_value = None + + captured_output = StringIO() + sys.stdout = captured_output + self.workflow.gather_subscriber_email_addresses() + sys.stdout = sys.__stdout__ + + email_variants = get_subaddress_variants("user@example.com", 3) + expected_output = ( + "\n".join( + [ + f"Contact with email '{email}' created successfully.\nWelcome email sent to '{email}'." + for email in email_variants + ] + ) + + "\n" + ) + self.assertEqual(captured_output.getvalue(), expected_output) + + @patch("builtins.input", return_value="user@example.com") + def test_gather_subscriber_email_addresses_error_contact_exists(self, mock_input): + self.ses_client.create_contact.side_effect = [ + None, + None, + ClientError( + error_response={"Error": {"Code": "AlreadyExistsException"}}, + operation_name="CreateContact", + ), + ] + self.ses_client.send_email.return_value = None + + captured_output = StringIO() + sys.stdout = captured_output + self.workflow.gather_subscriber_email_addresses() + sys.stdout = sys.__stdout__ + + email_variants = get_subaddress_variants("user@example.com", 3) + expected_output = f"Contact with email '{email_variants[0]}' created successfully.\nWelcome email sent to '{email_variants[0]}'.\nContact with email '{email_variants[1]}' created successfully.\nWelcome email sent to '{email_variants[1]}'.\nContact with email '{email_variants[2]}' already exists. Skipping...\n" + self.assertEqual(captured_output.getvalue(), expected_output) + + @patch("builtins.input", return_value="user@example.com") + def test_gather_subscriber_email_addresses_error_send_email_failed( + self, mock_input + ): + self.ses_client.create_contact.return_value = None + self.ses_client.send_email.side_effect = [ + None, + ClientError( + error_response={"Error": {"Code": "MessageRejected"}}, + operation_name="SendEmail", + ), + None, + ] + + captured_output = StringIO() + sys.stdout = captured_output + with self.assertRaises(ClientError): + self.workflow.gather_subscriber_email_addresses() + sys.stdout = sys.__stdout__ + + email_variants = get_subaddress_variants("user@example.com", 3) + expected_output = f"Contact with email '{email_variants[0]}' created successfully.\nWelcome email sent to '{email_variants[0]}'.\nContact with email '{email_variants[1]}' created successfully.\n" + self.assertEqual(captured_output.getvalue(), expected_output) + + +class TestSESv2WorkflowSendCouponNewsletter(unittest.TestCase): + @patch("main.boto3.client") + def setUp(self, mock_client): + self.ses_client = mock_client() + self.workflow = SESv2Workflow(self.ses_client, sleep=False) + self.workflow.verified_email = "verified@example.com" + + # Tests for send_coupon_newsletter + @patch( + "main.load_file_content", + side_effect=[ + "Template Content", + "Plain Text Template Content", + '[{"details": "20% off on all electronics"}]', + ], + ) + def test_send_coupon_newsletter_success(self, mock_load_file_content): + self.ses_client.list_contacts.return_value = { + "Contacts": [ + {"EmailAddress": "user+1@example.com"}, + {"EmailAddress": "user+2@example.com"}, + {"EmailAddress": "user+3@example.com"}, + ] + } + self.ses_client.send_email.return_value = None + + captured_output = StringIO() + sys.stdout = captured_output + self.workflow.send_coupon_newsletter() + sys.stdout = sys.__stdout__ + + expected_output = "Newsletter sent to 'user+1@example.com'.\nNewsletter sent to 'user+2@example.com'.\nNewsletter sent to 'user+3@example.com'.\n" + self.assertEqual(captured_output.getvalue(), expected_output) + + def test_send_coupon_newsletter_error_contact_list_not_found(self): + self.ses_client.list_contacts.side_effect = ClientError( + error_response={"Error": {"Code": "NotFoundException"}}, + operation_name="ListContacts", + ) + + captured_output = StringIO() + sys.stdout = captured_output + self.workflow.send_coupon_newsletter() + sys.stdout = sys.__stdout__ + + expected_output = f"Contact list '{CONTACT_LIST_NAME}' does not exist.\n" + self.assertEqual(captured_output.getvalue(), expected_output) + + @patch("main.load_file_content", return_value=json.dumps(["Coupon 1", "Coupon 2"])) + def test_send_coupon_newsletter_error_send_email_failed( + self, mock_load_file_content + ): + self.ses_client.list_contacts.return_value = { + "Contacts": [ + {"EmailAddress": "user1@example.com"}, + {"EmailAddress": "user2@example.com"}, + {"EmailAddress": "user3@example.com"}, + ] + } + self.ses_client.send_email.side_effect = [ + None, + ClientError( + error_response={"Error": {"Code": "MessageRejected"}}, + operation_name="SendEmail", + ), + None, + ] + + captured_output = StringIO() + sys.stdout = captured_output + self.workflow.send_coupon_newsletter() + sys.stdout = sys.__stdout__ + + expected_output = "Newsletter sent to 'user1@example.com'.\nError: An error occurred (MessageRejected) when calling the SendEmail operation: Unknown\nNewsletter sent to 'user3@example.com'.\n" + self.assertEqual(captured_output.getvalue(), expected_output) + + +class TestSESv2WorkflowCleanUp(unittest.TestCase): + @patch("main.boto3.client") + def setUp(self, mock_client): + self.ses_client = mock_client() + self.workflow = SESv2Workflow(self.ses_client, sleep=False) + self.workflow.verified_email = "verified@example.com" + + # Tests for clean_up + @patch("builtins.input", return_value="n") + def test_clean_up_success(self, mock_input): + self.ses_client.delete_contact_list.return_value = None + self.ses_client.delete_email_template.return_value = None + self.ses_client.delete_email_identity.return_value = None + + captured_output = StringIO() + sys.stdout = captured_output + self.workflow.clean_up() + sys.stdout = sys.__stdout__ + + expected_output = f"Contact list '{CONTACT_LIST_NAME}' deleted successfully.\nEmail template '{TEMPLATE_NAME}' deleted successfully.\nSkipping email identity deletion.\n" + self.assertEqual(captured_output.getvalue(), expected_output) + + @patch("builtins.input", return_value="n") + def test_clean_up_error_contact_list_not_found(self, mock_input): + self.ses_client.delete_contact_list.side_effect = ClientError( + error_response={"Error": {"Code": "NotFoundException"}}, + operation_name="DeleteContactList", + ) + self.ses_client.delete_email_template.return_value = None + self.ses_client.delete_email_identity.return_value = None + + captured_output = StringIO() + sys.stdout = captured_output + self.workflow.clean_up() + sys.stdout = sys.__stdout__ + + expected_output = f"Contact list '{CONTACT_LIST_NAME}' does not exist.\nEmail template '{TEMPLATE_NAME}' deleted successfully.\nSkipping email identity deletion.\n" + self.assertEqual(captured_output.getvalue(), expected_output) + + @patch("builtins.input", return_value="n") + def test_clean_up_error_template_not_found(self, mock_input): + self.ses_client.delete_contact_list.return_value = None + self.ses_client.delete_email_template.side_effect = ClientError( + error_response={"Error": {"Code": "NotFoundException"}}, + operation_name="DeleteEmailTemplate", + ) + self.ses_client.delete_email_identity.return_value = None + + captured_output = StringIO() + sys.stdout = captured_output + self.workflow.clean_up() + sys.stdout = sys.__stdout__ + + expected_output = f"Contact list '{CONTACT_LIST_NAME}' deleted successfully.\nEmail template '{TEMPLATE_NAME}' does not exist.\nSkipping email identity deletion.\n" + self.assertEqual(captured_output.getvalue(), expected_output) + + @patch("builtins.input", return_value="y") + def test_clean_up_error_identity_not_found(self, mock_input): + self.ses_client.delete_contact_list.return_value = None + self.ses_client.delete_email_template.return_value = None + self.ses_client.delete_email_identity.side_effect = ClientError( + error_response={"Error": {"Code": "NotFoundException"}}, + operation_name="DeleteEmailIdentity", + ) + + captured_output = StringIO() + sys.stdout = captured_output + self.workflow.clean_up() + sys.stdout = sys.__stdout__ + + expected_output = f"Contact list '{CONTACT_LIST_NAME}' deleted successfully.\nEmail template '{TEMPLATE_NAME}' deleted successfully.\nEmail identity '{self.workflow.verified_email}' does not exist.\n" + self.assertEqual(captured_output.getvalue(), expected_output) diff --git a/python/example_code/sesv2/requirements.txt b/python/example_code/sesv2/requirements.txt new file mode 100644 index 00000000000..621e276912d --- /dev/null +++ b/python/example_code/sesv2/requirements.txt @@ -0,0 +1,2 @@ +boto3>=1.26.79 +pytest>=7.2.1 diff --git a/rustv1/examples/ses/Cargo.toml b/rustv1/examples/ses/Cargo.toml index 9f54e96bdc4..8df3b411249 100644 --- a/rustv1/examples/ses/Cargo.toml +++ b/rustv1/examples/ses/Cargo.toml @@ -9,7 +9,13 @@ edition = "2021" [dependencies] aws-config = { version = "1.0.1", features = ["behavior-version-latest"] } -aws-sdk-sesv2 = { version = "1.3.0" } +aws-sdk-sesv2 = { version = "1.3.0", features = ["test-util"]} tokio = { version = "1.20.1", features = ["full"] } clap = { version = "~4.4", features = ["derive"] } tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } +anyhow = "1.0.81" +tracing = "0.1.40" +tmpfile = "0.0.2" +aws-smithy-http = "0.60.7" +aws-smithy-mocks-experimental = "0.2.0" +open = "5.1.2" diff --git a/rustv1/examples/ses/README.md b/rustv1/examples/ses/README.md index 455bb5dd83f..da58ad0ce84 100644 --- a/rustv1/examples/ses/README.md +++ b/rustv1/examples/ses/README.md @@ -1,20 +1,20 @@ -# Amazon SES code examples for the SDK for Rust +# Amazon SES v2 API code examples for the SDK for Rust ## Overview -Shows how to use the AWS SDK for Rust to work with Amazon Simple Email Service (Amazon SES). +Shows how to use the AWS SDK for Rust to work with Amazon Simple Email Service v2 API. -_Amazon SES is a reliable, scalable, and cost-effective email service._ +_Amazon SES v2 API is a reliable, scalable, and cost-effective email service._ ## ⚠ Important -* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). -* Running the tests might result in charges to your AWS account. -* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). -* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). +- Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +- Running the tests might result in charges to your AWS account. +- We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +- This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). @@ -25,10 +25,26 @@ _Amazon SES is a reliable, scalable, and cost-effective email service._ For prerequisites, see the [README](../../README.md#Prerequisites) in the `rustv1` folder. - +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [Create a contact in a contact list](src/bin/create-contact.rs#L30) (`CreateContact`) +- [Create a contact list](src/bin/create-contact-list.rs#L26) (`CreateContactList`) +- [Create an email identity](src/newsletter.rs#L49) (`CreateEmailIdentity`) +- [Create an email template](src/newsletter.rs#L94) (`CreateEmailTemplate`) +- [Delete a contact list](src/newsletter.rs#L338) (`DeleteContactList`) +- [Delete an email identity](src/newsletter.rs#L376) (`DeleteEmailIdentity`) +- [Delete an email template](src/newsletter.rs#L351) (`DeleteEmailTemplate`) +- [Get identity information](src/bin/is-email-verified.rs#L26) (`GetEmailIdentity`) +- [List the contact lists](src/bin/list-contact-lists.rs#L22) (`ListContactLists`) +- [List the contacts in a contact list](src/bin/list-contacts.rs#L26) (`ListContacts`) +- [Send a simple email](src/bin/send-email.rs#L39) (`SendEmail`) +- [Send a templated email](src/newsletter.rs#L255) (`SendEmail`) + @@ -36,30 +52,27 @@ For prerequisites, see the [README](../../README.md#Prerequisites) in the `rustv ### Instructions - - +To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer/resources into a new folder, rustv1/examples/ses/resources/newsletter. + ### Tests ⚠ Running tests might result in charges to your AWS account. - To find instructions for running these tests, see the [README](../../README.md#Tests) in the `rustv1` folder. - - ## Additional resources -- [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/Welcome.html) -- [Amazon SES API Reference](https://docs.aws.amazon.com/ses/latest/APIReference/Welcome.html) -- [SDK for Rust Amazon SES reference](https://docs.rs/aws-sdk-ses/latest/aws_sdk_ses/) +- [Amazon SES v2 API Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/Welcome.html) +- [Amazon SES v2 API API Reference](https://docs.aws.amazon.com/ses/latest/APIReference-V2/Welcome.html) +- [SDK for Rust Amazon SES v2 API reference](https://docs.rs/aws-sdk-ses/latest/aws_sdk_ses/) @@ -68,4 +81,4 @@ in the `rustv1` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: Apache-2.0 diff --git a/rustv1/examples/ses/src/bin/newsletter.rs b/rustv1/examples/ses/src/bin/newsletter.rs new file mode 100644 index 00000000000..5b850a8df40 --- /dev/null +++ b/rustv1/examples/ses/src/bin/newsletter.rs @@ -0,0 +1,45 @@ +use anyhow::{anyhow, Result}; +use aws_sdk_sesv2::Client; +use ses_code_examples::newsletter::SESWorkflow; + +const INTRO: &str = " +Welcome to the Amazon SES v2 Coupon Newsletter Workflow! + +This workflow will help you: +1. Prepare a verified email identity and contact list for your newsletter. +2. Gather subscriber email addresses and send them a welcome email. +3. Send a weekly coupon newsletter to your subscribers using email templates. +4. Monitor your sending activity and metrics in the AWS console. + +Let's get started! +"; + +/// The main entry point of the newsletter workflow. +/// +/// # Arguments +/// +/// None +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + println!("{}", INTRO); + + // Initialize the SES client + let config = aws_config::from_env().load().await; + let client = Client::new(&config); + + // Initialize the SESWorkflow struct + let mut stdin = std::io::stdin().lock(); + let mut stdout = std::io::stdout().lock(); + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + + // Execute the workflow steps + let run = workflow.run().await; + if let Err(e) = run { + return Err(anyhow!("Error in run: {e}")); + } + let _ = workflow.cleanup().await; + + Ok(()) +} diff --git a/rustv1/examples/ses/src/lib.rs b/rustv1/examples/ses/src/lib.rs new file mode 100644 index 00000000000..688839d67e3 --- /dev/null +++ b/rustv1/examples/ses/src/lib.rs @@ -0,0 +1 @@ +pub mod newsletter; diff --git a/rustv1/examples/ses/src/newsletter.rs b/rustv1/examples/ses/src/newsletter.rs new file mode 100644 index 00000000000..fe82a144f8c --- /dev/null +++ b/rustv1/examples/ses/src/newsletter.rs @@ -0,0 +1,410 @@ +use anyhow::{anyhow, Result}; +use aws_sdk_sesv2::{ + types::{ + Body, Contact, Content, Destination, EmailContent, EmailTemplateContent, + ListManagementOptions, Message, Template, + }, + Client, +}; +use std::io::{BufRead, Write}; +use tracing::info; + +const CONTACT_LIST_NAME: &str = "weekly-coupons-newsletter"; +const TEMPLATE_NAME: &str = "weekly-coupons"; + +/// The SESWorkflow struct encapsulates the entire SES v2 Coupon Newsletter Workflow. +pub struct SESWorkflow<'a> { + client: Client, + stdin: &'a mut dyn BufRead, + stdout: &'a mut dyn Write, + verified_email: String, +} + +impl<'a> SESWorkflow<'a> { + /// Creates a new instance of the SESWorkflow struct. + /// + /// # Arguments + /// + /// * `client` - The AWS SDK for Rust SES v2 client. + /// * `stdin` - A mutable reference to the standard input stream. + /// * `stdout` - A mutable reference to the standard output stream. + pub fn new(client: Client, stdin: &'a mut dyn BufRead, stdout: &'a mut dyn Write) -> Self { + Self { + client, + stdin, + stdout, + verified_email: "".into(), + } + } + + /// Prepares the application by creating a verified email identity and a contact list. + pub async fn prepare_application(&mut self) -> Result<()> { + // Prompt the user for a verified email address + writeln!(self.stdout, "Enter the verified email address to use: ")?; + self.stdout.flush().unwrap(); + let mut verified_email = String::new(); + self.stdin.read_line(&mut verified_email).unwrap(); + self.verified_email = verified_email.trim().to_string(); + + // snippet-start:[sesv2.rust.create-email-identity] + match self + .client + .create_email_identity() + .email_identity(self.verified_email.clone()) + .send() + .await + { + Ok(_) => writeln!(self.stdout, "Email identity created successfully.")?, + Err(e) => { + if e.as_service_error().unwrap().is_already_exists_exception() { + writeln!( + self.stdout, + "Email identity already exists, skipping creation." + )?; + } else { + return Err(anyhow!("Error creating email identity: {}", e)); + } + } + } + // snippet-end:[sesv2.rust.create-email-identity] + + // Create the contact list + // snippet-start:[sesv2.rust.create-contact-list] + match self + .client + .create_contact_list() + .contact_list_name(CONTACT_LIST_NAME) + .send() + .await + { + Ok(_) => writeln!(self.stdout, "Contact list created successfully.")?, + Err(e) => { + if e.as_service_error().unwrap().is_already_exists_exception() { + writeln!( + self.stdout, + "Contact list already exists, skipping creation." + )?; + } else { + return Err(anyhow!("Error creating contact list: {}", e)); + } + } + } + // snippet-end:[sesv2.rust.create-contact-list] + + // snippet-start:[sesv2.rust.create-email-template] + let TEMPLATE_HTML: &str = include_str!("../resources/newsletter/coupon-newsletter.html"); + let TEMPLATE_TEXT: &str = include_str!("../resources/newsletter/coupon-newsletter.txt"); + + // Create the email template + let template_content = EmailTemplateContent::builder() + .subject("Weekly Coupons Newsletter") + .html(TEMPLATE_HTML) + .text(TEMPLATE_TEXT) + .build(); + + match self + .client + .create_email_template() + .template_name(TEMPLATE_NAME) + .template_content(template_content) + .send() + .await + { + Ok(_) => writeln!(self.stdout, "Email template created successfully.")?, + Err(e) => { + if e.as_service_error().unwrap().is_already_exists_exception() { + writeln!( + self.stdout, + "Email template already exists, skipping creation." + )?; + } else { + return Err(anyhow!("Error creating email template: {}", e)); + } + } + } + // snippet-end:[sesv2.rust.create-email-template] + + Ok(()) + } + + /// Gathers subscriber email addresses and sends welcome emails. + pub async fn gather_subscriber_emails(&mut self) -> Result<()> { + // Prompt the user for a base email address + writeln!( + self.stdout, + "Enter a base email address for subscribing (e.g., user@example.com): " + )?; + self.stdout.flush().unwrap(); + let mut base_email = String::new(); + self.stdin.read_line(&mut base_email).unwrap(); + let base_email = base_email.trim().to_string(); + + // Create 3 variants of the email address as {user email}+ses-weekly-newsletter-{i}@{user domain} + let (user_email, user_domain) = base_email.split_once('@').unwrap(); + let mut emails = Vec::with_capacity(3); + for i in 1..=3 { + let email = format!("{}+ses-weekly-newsletter-{}@{}", user_email, i, user_domain); + emails.push(email); + } + + // Create a contact and send a welcome email for each email address + for email in emails { + // Create the contact + // snippet-start:[sesv2.rust.create-contact] + match self + .client + .create_contact() + .contact_list_name(CONTACT_LIST_NAME) + .email_address(email.clone()) + .send() + .await + { + Ok(_) => writeln!(self.stdout, "Contact created for {}", email)?, + Err(e) => { + if e.as_service_error().unwrap().is_already_exists_exception() { + writeln!( + self.stdout, + "Contact already exists for {}, skipping creation.", + email + )?; + } else { + return Err(anyhow!("Error creating contact for {}: {}", email, e)); + } + } + } + // snippet-end:[sesv2.rust.create-contact] + + // Send the welcome email + // snippet-start:[sesv2.rust.send-email.simple] + let WELCOME_HTML: &str = include_str!("../resources/newsletter/welcome.html"); + let WELCOME_TXT: &str = include_str!("../resources/newsletter/welcome.txt"); + let email_content = EmailContent::builder() + .simple( + Message::builder() + .subject( + Content::builder() + .data("Welcome to the Weekly Coupons Newsletter") + .build()?, + ) + .body( + Body::builder() + .html(Content::builder().data(WELCOME_HTML).build()?) + .text(Content::builder().data(WELCOME_TXT).build()?) + .build(), + ) + .build(), + ) + .build(); + + match self + .client + .send_email() + .from_email_address(self.verified_email.clone()) + .destination(Destination::builder().to_addresses(email.clone()).build()) + .content(email_content) + .send() + .await + { + Ok(output) => { + if let Some(message_id) = output.message_id { + writeln!( + self.stdout, + "Welcome email sent to {} with message ID {}", + email, message_id + )?; + } else { + writeln!(self.stdout, "Welcome email sent to {}", email)?; + } + } + Err(e) => return Err(anyhow!("Error sending welcome email to {}: {}", email, e)), + } + // snippet-end:[sesv2.rust.send-email.simple] + } + + Ok(()) + } + + /// Sends the coupon newsletter to the subscribers. + pub async fn send_coupon_newsletter(&mut self) -> Result<()> { + // Retrieve the list of contacts + // snippet-start:[sesv2.rust.list-contacts] + let contacts: Vec = match self + .client + .list_contacts() + .contact_list_name(CONTACT_LIST_NAME) + .send() + .await + { + Ok(list_contacts_output) => { + list_contacts_output.contacts.unwrap().into_iter().collect() + } + Err(e) => { + return Err(anyhow!( + "Error retrieving contact list {}: {}", + CONTACT_LIST_NAME, + e + )) + } + }; + // snippet-end:[sesv2.rust.list-contacts] + + // Send the newsletter to each contact + for email in contacts { + let email = email.email_address.unwrap(); + + // snippet-start:[sesv2.rust.send-email.template] + let COUPONS: &str = include_str!("../resources/newsletter/sample_coupons.json"); + let email_content = EmailContent::builder() + .template( + Template::builder() + .template_name(TEMPLATE_NAME) + .template_data(COUPONS) + .build(), + ) + .build(); + + match self + .client + .send_email() + .from_email_address(self.verified_email.clone()) + .destination(Destination::builder().to_addresses(email.clone()).build()) + .content(email_content) + .list_management_options( + ListManagementOptions::builder() + .contact_list_name(CONTACT_LIST_NAME) + .build()?, + ) + .send() + .await + { + Ok(output) => { + if let Some(message_id) = output.message_id { + writeln!( + self.stdout, + "Newsletter sent to {} with message ID {}", + email, message_id + )?; + } else { + writeln!(self.stdout, "Newsletter sent to {}", email)?; + } + } + Err(e) => return Err(anyhow!("Error sending newsletter to {}: {}", email, e)), + } + // snippet-end:[sesv2.rust.send-email.template] + } + + Ok(()) + } + + /// Monitors the sending activity and provides insights. + pub async fn monitor(&mut self) -> Result<()> { + // Check if the user wants to review the monitoring dashboard + writeln!( + self.stdout, + "Do you want to review the monitoring dashboard? (y/n): " + )?; + self.stdout.flush().unwrap(); + let mut response = String::new(); + self.stdin.read_line(&mut response).unwrap(); + + if response.trim().eq_ignore_ascii_case("y") { + // Open the SES monitoring dashboard in the default browser + open::that("https://console.aws.amazon.com/ses/home#/account")?; + + writeln!( + self.stdout, + "The SES monitoring dashboard has been opened in your default browser." + )?; + writeln!( + self.stdout, + "Review the sending activity, open and click rates, bounces, complaints, and more." + )?; + } else { + writeln!(self.stdout, "Skipping the monitoring dashboard review.")?; + } + + writeln!(self.stdout, "Press any key to continue.")?; + self.stdout.flush().unwrap(); + let mut response = String::new(); + self.stdin.read_line(&mut response).unwrap(); + + Ok(()) + } + + /// Cleans up the resources created during the workflow. + pub async fn cleanup(&mut self) -> Result<()> { + info!("Cleaning up resources..."); + + // snippet-start:[sesv2.rust.delete-contact-list] + match self + .client + .delete_contact_list() + .contact_list_name(CONTACT_LIST_NAME) + .send() + .await + { + Ok(_) => writeln!(self.stdout, "Contact list deleted successfully.")?, + Err(e) => return Err(anyhow!("Error deleting contact list: {e}")), + } + // snippet-end:[sesv2.rust.delete-contact-list] + + // snippet-start:[sesv2.rust.delete-email-template] + match self + .client + .delete_email_template() + .template_name(TEMPLATE_NAME) + .send() + .await + { + Ok(_) => writeln!(self.stdout, "Email template deleted successfully.")?, + Err(e) => { + return Err(anyhow!("Error deleting email template: {e}")); + } + } + // snippet-end:[sesv2.rust.delete-email-template] + + // Delete the email identity + writeln!( + self.stdout, + "Do you want to delete the verified email identity? (y/n): " + )?; + self.stdout.flush().unwrap(); + let mut response = String::new(); + self.stdin.read_line(&mut response).unwrap(); + + if response.trim().eq_ignore_ascii_case("y") { + // snippet-start:[sesv2.rust.delete-email-identity] + match self + .client + .delete_email_identity() + .email_identity(self.verified_email.clone()) + .send() + .await + { + Ok(_) => writeln!(self.stdout, "Email identity deleted successfully.")?, + Err(e) => { + return Err(anyhow!("Error deleting email identity: {}", e)); + } + } + // snippet-end:[sesv2.rust.delete-email-identity] + } else { + writeln!(self.stdout, "Skipping deletion of email identity.")?; + } + + info!("Cleanup completed."); + + Ok(()) + } + + pub async fn run(&mut self) -> Result<()> { + self.prepare_application().await?; + self.gather_subscriber_emails().await?; + self.send_coupon_newsletter().await?; + self.monitor().await?; + Ok(()) + } + + pub fn set_verified_email(&mut self, verified_email: String) { + self.verified_email = verified_email; + } +} diff --git a/rustv1/examples/ses/tests/test_newsletter.rs b/rustv1/examples/ses/tests/test_newsletter.rs new file mode 100644 index 00000000000..5e0f98d0106 --- /dev/null +++ b/rustv1/examples/ses/tests/test_newsletter.rs @@ -0,0 +1,832 @@ +use std::io::Cursor; + +use anyhow::Result; +use aws_sdk_sesv2::operation::create_contact::{CreateContactError, CreateContactOutput}; +use aws_sdk_sesv2::operation::create_contact_list::{ + CreateContactListError, CreateContactListOutput, +}; +use aws_sdk_sesv2::operation::create_email_identity::{ + CreateEmailIdentityError, CreateEmailIdentityOutput, +}; +use aws_sdk_sesv2::operation::create_email_template::CreateEmailTemplateOutput; +use aws_sdk_sesv2::operation::delete_contact_list::{ + DeleteContactListError, DeleteContactListOutput, +}; +use aws_sdk_sesv2::operation::delete_email_identity::DeleteEmailIdentityError; +use aws_sdk_sesv2::operation::delete_email_template::{ + DeleteEmailTemplateError, DeleteEmailTemplateOutput, +}; +use aws_sdk_sesv2::operation::list_contacts::{ListContactsError, ListContactsOutput}; +use aws_sdk_sesv2::operation::send_email::{SendEmailError, SendEmailOutput}; +use aws_sdk_sesv2::types::error::{ + AccountSuspendedException, AlreadyExistsException, LimitExceededException, + MailFromDomainNotVerifiedException, MessageRejected, NotFoundException, SendingPausedException, +}; +use aws_sdk_sesv2::types::{Contact, IdentityType}; +use aws_sdk_sesv2::Client; +use aws_smithy_mocks_experimental::{mock, mock_client, RuleMode}; +use ses_code_examples::newsletter::SESWorkflow; + +// Test prepare_application method +#[tokio::test] +async fn test_prepare_application_success() -> Result<()> { + let mut stdin = Cursor::new("test@example.com\n".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_email_identity = mock!(Client::create_email_identity) + .match_requests(|req| req.email_identity() == Some("test@example.com")) + .then_output(|| { + CreateEmailIdentityOutput::builder() + .identity_type(IdentityType::EmailAddress) + .verified_for_sending_status(true) + .build() + }); + + let mock_contact_list = mock!(Client::create_contact_list) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_output(|| CreateContactListOutput::builder().build()); + + let mock_email_template = mock!(Client::create_email_template) + .match_requests(|req| req.template_name() == Some("weekly-coupons")) + .then_output(|| CreateEmailTemplateOutput::builder().build()); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[ + &mock_email_identity, + &mock_contact_list, + &mock_email_template, + ] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + + // Run the method + workflow.prepare_application().await?; + + // Assert the output + let output = String::from_utf8(stdout)?; + assert!(output.contains("Email identity created successfully.")); + assert!(output.contains("Contact list created successfully.")); + assert!(output.contains("Email template created successfully.")); + + Ok(()) +} + +#[tokio::test] +async fn test_prepare_application_error_identity_already_exists() -> Result<()> { + let mut stdin = Cursor::new("test@example.com\n".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_email_identity = mock!(Client::create_email_identity) + .match_requests(|req| req.email_identity() == Some("test@example.com")) + .then_error(|| { + CreateEmailIdentityError::AlreadyExistsException( + AlreadyExistsException::builder().build(), + ) + }); + + let mock_contact_list = mock!(Client::create_contact_list) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_output(|| CreateContactListOutput::builder().build()); + + let mock_email_template = mock!(Client::create_email_template) + .match_requests(|req| req.template_name() == Some("weekly-coupons")) + .then_output(|| CreateEmailTemplateOutput::builder().build()); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[ + &mock_email_identity, + &mock_contact_list, + &mock_email_template, + ] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + + // Run the method + workflow.prepare_application().await?; + + // Assert the output + let output = String::from_utf8(stdout)?; + assert!(output.contains("Email identity already exists, skipping creation.")); + assert!(output.contains("Contact list created successfully.")); + assert!(output.contains("Email template created successfully.")); + + Ok(()) +} + +#[tokio::test] +async fn test_prepare_application_error_identity_not_found() -> Result<()> { + let mut stdin = Cursor::new("test@example.com\n".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_email_identity = mock!(Client::create_email_identity) + .match_requests(|req| req.email_identity() == Some("test@example.com")) + .then_error(|| { + CreateEmailIdentityError::NotFoundException(NotFoundException::builder().build()) + }); + + let client = mock_client!(aws_sdk_sesv2, RuleMode::Sequential, &[&mock_email_identity]); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + + // Run the method + let result = workflow.prepare_application().await; + + // Check that the error is propagated + assert!(result.is_err()); + assert!(format!("{result:?}").contains("Error creating email identity:")); + + Ok(()) +} + +#[tokio::test] +async fn test_prepare_application_error_identity_limit_exceeded() -> Result<()> { + let mut stdin = Cursor::new("test@example.com\n".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_email_identity = mock!(Client::create_email_identity) + .match_requests(|req| req.email_identity() == Some("test@example.com")) + .then_error(|| { + CreateEmailIdentityError::LimitExceededException( + LimitExceededException::builder().build(), + ) + }); + + let client = mock_client!(aws_sdk_sesv2, RuleMode::Sequential, &[&mock_email_identity]); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + + // Run the method + let result = workflow.prepare_application().await; + + // Check that the error is propagated + assert!(result.is_err()); + assert!(format!("{result:?}").contains("Error creating email identity:")); + + Ok(()) +} + +#[tokio::test] +async fn test_prepare_application_error_contact_list_limit_exceeded() -> Result<()> { + let mut stdin = Cursor::new("test@example.com\n".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_email_identity = mock!(Client::create_email_identity) + .match_requests(|req| req.email_identity() == Some("test@example.com")) + .then_output(|| { + CreateEmailIdentityOutput::builder() + .identity_type(IdentityType::EmailAddress) + .verified_for_sending_status(true) + .build() + }); + + let mock_contact_list = mock!(Client::create_contact_list) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_error(|| { + CreateContactListError::LimitExceededException( + LimitExceededException::builder().build(), + ) + }); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[&mock_email_identity, &mock_contact_list] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + + // Run the method + let result = workflow.prepare_application().await; + + // Assert the output + let output = String::from_utf8(stdout)?; + assert!(output.contains("Email identity created successfully.")); + + // Check that the error is propagated + assert!(result.is_err()); + assert!(format!("{result:?}").contains("Error creating contact list:")); + + Ok(()) +} + +#[tokio::test] +async fn test_gather_subscriber_emails_success() -> Result<()> { + let mut stdin = Cursor::new("user@example.com\n".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_create_contact_1 = mock!(Client::create_contact) + .match_requests(|req| { + req.email_address() == Some("user+ses-weekly-newsletter-1@example.com") + }) + .then_output(|| CreateContactOutput::builder().build()); + + let mock_create_contact_2 = mock!(Client::create_contact) + .match_requests(|req| { + req.email_address() == Some("user+ses-weekly-newsletter-2@example.com") + }) + .then_output(|| CreateContactOutput::builder().build()); + + let mock_create_contact_3 = mock!(Client::create_contact) + .match_requests(|req| { + req.email_address() == Some("user+ses-weekly-newsletter-3@example.com") + }) + .then_output(|| CreateContactOutput::builder().build()); + + let mock_send_email_1 = mock!(Client::send_email) + .match_requests(|req| { + req.destination() + .unwrap() + .to_addresses() + .contains(&"user+ses-weekly-newsletter-1@example.com".into()) + }) + .then_output(|| SendEmailOutput::builder().message_id("email-1").build()); + + let mock_send_email_2 = mock!(Client::send_email) + .match_requests(|req| { + req.destination() + .unwrap() + .to_addresses() + .contains(&"user+ses-weekly-newsletter-2@example.com".into()) + }) + .then_output(|| SendEmailOutput::builder().message_id("email-2").build()); + + let mock_send_email_3 = mock!(Client::send_email) + .match_requests(|req| { + req.destination() + .unwrap() + .to_addresses() + .contains(&"user+ses-weekly-newsletter-3@example.com".into()) + }) + .then_output(|| SendEmailOutput::builder().message_id("email-3").build()); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[ + &mock_create_contact_1, + &mock_send_email_1, + &mock_create_contact_2, + &mock_send_email_2, + &mock_create_contact_3, + &mock_send_email_3, + ] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + + // Run the method + workflow.gather_subscriber_emails().await?; + + // Assert the output + let output = String::from_utf8(stdout)?; + assert!(output.contains("Contact created for user+ses-weekly-newsletter-1@example.com")); + assert!(output.contains( + "Welcome email sent to user+ses-weekly-newsletter-1@example.com with message ID email-1" + )); + assert!(output.contains("Contact created for user+ses-weekly-newsletter-2@example.com")); + assert!(output.contains( + "Welcome email sent to user+ses-weekly-newsletter-2@example.com with message ID email-2" + )); + assert!(output.contains("Contact created for user+ses-weekly-newsletter-3@example.com")); + assert!(output.contains( + "Welcome email sent to user+ses-weekly-newsletter-3@example.com with message ID email-3" + )); + + Ok(()) +} + +#[tokio::test] +async fn test_gather_subscriber_emails_error_contact_already_exists() -> Result<()> { + let mut stdin = Cursor::new("user@example.com\n".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_create_contact_1 = mock!(Client::create_contact) + .match_requests(|req| { + req.email_address() == Some("user+ses-weekly-newsletter-1@example.com") + }) + .then_output(|| CreateContactOutput::builder().build()); + + let mock_send_welcome_1 = mock!(Client::send_email) + .match_requests(|req| { + req.destination() + .unwrap() + .to_addresses() + .contains(&"user+ses-weekly-newsletter-1@example.com".into()) + }) + .then_output(|| SendEmailOutput::builder().build()); + + let mock_create_contact_2 = mock!(Client::create_contact) + .match_requests(|req| { + req.email_address() == Some("user+ses-weekly-newsletter-2@example.com") + }) + .then_error(|| { + CreateContactError::AlreadyExistsException(AlreadyExistsException::builder().build()) + }); + + let mock_send_welcome_2 = mock!(Client::send_email) + .match_requests(|req| { + req.destination() + .unwrap() + .to_addresses() + .contains(&"user+ses-weekly-newsletter-2@example.com".into()) + }) + .then_output(|| SendEmailOutput::builder().build()); + + let mock_create_contact_3 = mock!(Client::create_contact) + .match_requests(|req| { + req.email_address() == Some("user+ses-weekly-newsletter-3@example.com") + }) + .then_output(|| CreateContactOutput::builder().build()); + + let mock_send_welcome_3 = mock!(Client::send_email) + .match_requests(|req| { + req.destination() + .unwrap() + .to_addresses() + .contains(&"user+ses-weekly-newsletter-3@example.com".into()) + }) + .then_output(|| SendEmailOutput::builder().build()); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[ + &mock_create_contact_1, + &mock_send_welcome_1, + &mock_create_contact_2, + &mock_send_welcome_2, + &mock_create_contact_3, + &mock_send_welcome_3, + ] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + + // Run the method + workflow.gather_subscriber_emails().await?; + + // Assert the output + let output = String::from_utf8(stdout)?; + assert!(output.contains("Contact created for user+ses-weekly-newsletter-1@example.com")); + assert!(output.contains( + "Contact already exists for user+ses-weekly-newsletter-2@example.com, skipping creation." + )); + assert!(output.contains("Contact created for user+ses-weekly-newsletter-3@example.com")); + + Ok(()) +} + +#[tokio::test] +async fn test_gather_subscriber_emails_error_send_email() -> Result<()> { + let mut stdin = Cursor::new("user@example.com\n".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_create_contact_1 = mock!(Client::create_contact) + .match_requests(|req| { + req.email_address() == Some("user+ses-weekly-newsletter-1@example.com") + }) + .then_output(|| CreateContactOutput::builder().build()); + + let mock_send_email_1 = mock!(Client::send_email) + .match_requests(|req| { + req.destination() + .unwrap() + .to_addresses() + .contains(&"user+ses-weekly-newsletter-1@example.com".into()) + }) + .then_error(|| { + SendEmailError::AccountSuspendedException(AccountSuspendedException::builder().build()) + }); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[&mock_create_contact_1, &mock_send_email_1,] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + + // Run the method + let result = workflow.gather_subscriber_emails().await; + + // Check that the error is propagated + assert!(result.is_err()); + assert!(format!("{result:?}") + .contains("Error sending welcome email to user+ses-weekly-newsletter-1@example.com:")); + + Ok(()) +} + +#[tokio::test] +async fn test_send_coupon_newsletter_success() -> Result<()> { + let mut stdin = Cursor::new("".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_list_contacts = mock!(Client::list_contacts) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_output(|| { + ListContactsOutput::builder() + .contacts(Contact::builder().email_address("user@example.com").build()) + .build() + }); + + let mock_send_email = mock!(Client::send_email) + .match_requests(|req| { + req.destination() + .unwrap() + .to_addresses() + .contains(&"user@example.com".into()) + }) + .then_output(|| { + SendEmailOutput::builder() + .message_id("newsletter-email") + .build() + }); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[&mock_list_contacts, &mock_send_email,] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + workflow.set_verified_email("sender@example.com".into()); + + // Run the method + workflow.send_coupon_newsletter().await?; + + // Assert the output + let output = String::from_utf8(stdout)?; + assert!(output.contains("Newsletter sent to user@example.com with message ID newsletter-email")); + + Ok(()) +} + +#[tokio::test] +async fn test_send_coupon_newsletter_error_template_already_exists() -> Result<()> { + let mut stdin = Cursor::new("".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_list_contacts = mock!(Client::list_contacts) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_error(|| ListContactsError::NotFoundException(NotFoundException::builder().build())); + + let client = mock_client!(aws_sdk_sesv2, RuleMode::Sequential, &[&mock_list_contacts]); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + workflow.set_verified_email("sender@example.com".into()); + + // Run the method + let result = workflow.send_coupon_newsletter().await; + + // Check that the error is propagated + assert!(result.is_err()); + assert!( + format!("{result:?}").contains("Error retrieving contact list weekly-coupons-newsletter:") + ); + + Ok(()) +} + +#[tokio::test] +async fn test_send_coupon_newsletter_error_account_suspended() -> Result<()> { + let mut stdin = Cursor::new("".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_list_contacts = mock!(Client::list_contacts) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_output(|| { + ListContactsOutput::builder() + .contacts(Contact::builder().email_address("user@example.com").build()) + .build() + }); + + let mock_send_email = mock!(Client::send_email) + .match_requests(|req| { + req.destination() + .unwrap() + .to_addresses() + .contains(&"user@example.com".into()) + }) + .then_error(|| { + SendEmailError::AccountSuspendedException(AccountSuspendedException::builder().build()) + }); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[&mock_list_contacts, &mock_send_email,] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + workflow.set_verified_email("sender@example.com".into()); + + // Run the method + let result = workflow.send_coupon_newsletter().await; + + // Check that the error is propagated + assert!(result.is_err()); + assert!(format!("{result:?}").contains("Error sending newsletter to user@example.com:")); + + Ok(()) +} + +#[tokio::test] +async fn test_send_coupon_newsletter_error_mail_from_domain_not_verified() -> Result<()> { + let mut stdin = Cursor::new("".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_list_contacts = mock!(Client::list_contacts) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_output(|| { + ListContactsOutput::builder() + .contacts(Contact::builder().email_address("user@example.com").build()) + .build() + }); + + let mock_send_email = mock!(Client::send_email) + .match_requests(|req| { + req.destination() + .unwrap() + .to_addresses() + .contains(&"user@example.com".into()) + }) + .then_error(|| { + SendEmailError::MailFromDomainNotVerifiedException( + MailFromDomainNotVerifiedException::builder().build(), + ) + }); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[&mock_list_contacts, &mock_send_email,] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + workflow.set_verified_email("sender@example.com".into()); + + // Run the method + let result = workflow.send_coupon_newsletter().await; + + // Check that the error is propagated + assert!(result.is_err()); + assert!(format!("{result:?}").contains("Error sending newsletter to user@example.com:")); + + Ok(()) +} + +#[tokio::test] +async fn test_send_coupon_newsletter_error_message_rejected() -> Result<()> { + let mut stdin = Cursor::new("".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_list_contacts = mock!(Client::list_contacts) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_output(|| { + ListContactsOutput::builder() + .contacts(Contact::builder().email_address("user@example.com").build()) + .build() + }); + + let mock_send_email = mock!(Client::send_email) + .match_requests(|req| { + req.destination() + .unwrap() + .to_addresses() + .contains(&"user@example.com".into()) + }) + .then_error(|| SendEmailError::MessageRejected(MessageRejected::builder().build())); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[&mock_list_contacts, &mock_send_email,] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + workflow.set_verified_email("sender@example.com".into()); + + // Run the method + let result = workflow.send_coupon_newsletter().await; + + // Check that the error is propagated + assert!(result.is_err()); + assert!(format!("{result:?}").contains("Error sending newsletter to user@example.com:")); + + Ok(()) +} + +#[tokio::test] +async fn test_send_coupon_newsletter_error_sending_paused() -> Result<()> { + let mut stdin = Cursor::new("".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_list_contacts = mock!(Client::list_contacts) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_output(|| { + ListContactsOutput::builder() + .contacts(Contact::builder().email_address("user@example.com").build()) + .build() + }); + + let mock_send_email = mock!(Client::send_email) + .match_requests(|req| { + req.destination() + .unwrap() + .to_addresses() + .contains(&"user@example.com".into()) + }) + .then_error(|| { + SendEmailError::SendingPausedException(SendingPausedException::builder().build()) + }); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[&mock_list_contacts, &mock_send_email,] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + workflow.set_verified_email("sender@example.com".into()); + + // Run the method + let result = workflow.send_coupon_newsletter().await; + + // Check that the error is propagated + assert!(result.is_err()); + assert!(format!("{result:?}").contains("Error sending newsletter to user@example.com:")); + + Ok(()) +} +#[tokio::test] +async fn test_cleanup_success() -> Result<()> { + let mut stdin = Cursor::new("n\n".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_delete_contact_list = mock!(Client::delete_contact_list) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_output(|| DeleteContactListOutput::builder().build()); + + let mock_delete_email_template = mock!(Client::delete_email_template) + .match_requests(|req| req.template_name() == Some("weekly-coupons")) + .then_output(|| DeleteEmailTemplateOutput::builder().build()); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[&mock_delete_contact_list, &mock_delete_email_template,] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + + // Run the method + workflow.cleanup().await?; + + // Assert the output + let output = String::from_utf8(stdout)?; + assert!(output.contains("Contact list deleted successfully.")); + assert!(output.contains("Email template deleted successfully.")); + assert!(output.contains("Skipping deletion of email identity.")); + + Ok(()) +} + +#[tokio::test] +async fn test_cleanup_error_contact_list_not_found() -> Result<()> { + let mut stdin = Cursor::new("n\n".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_delete_contact_list = mock!(Client::delete_contact_list) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_error(|| { + DeleteContactListError::NotFoundException(NotFoundException::builder().build()) + }); + + let mock_delete_email_template = mock!(Client::delete_email_template) + .match_requests(|req| req.template_name() == Some("weekly-coupons")) + .then_output(|| DeleteEmailTemplateOutput::builder().build()); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[&mock_delete_contact_list, &mock_delete_email_template,] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + + // Run the method + let result = workflow.cleanup().await; + + // Assert the output + assert!(result.is_err()); + assert!(format!("{result:?}").contains("Error deleting contact list:")); + + Ok(()) +} + +#[tokio::test] +async fn test_cleanup_error_template_not_found() -> Result<()> { + let mut stdin = Cursor::new("n\n".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_delete_contact_list = mock!(Client::delete_contact_list) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_output(|| DeleteContactListOutput::builder().build()); + + let mock_delete_email_template = mock!(Client::delete_email_template) + .match_requests(|req| req.template_name() == Some("weekly-coupons")) + .then_error(|| { + DeleteEmailTemplateError::NotFoundException(NotFoundException::builder().build()) + }); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[&mock_delete_contact_list, &mock_delete_email_template,] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + + // Run the method + let result = workflow.cleanup().await; + + // Assert the output + let output = String::from_utf8(stdout)?; + assert!(output.contains("Contact list deleted successfully.")); + + assert!(result.is_err()); + assert!(format!("{result:?}").contains("Error deleting email template:")); + + Ok(()) +} + +#[tokio::test] +async fn test_cleanup_error_identity_not_found() -> Result<()> { + let mut stdin = Cursor::new("y\n".as_bytes()); + let mut stdout = Vec::::new(); + + // Mock AWS SDK calls + let mock_delete_contact_list = mock!(Client::delete_contact_list) + .match_requests(|req| req.contact_list_name() == Some("weekly-coupons-newsletter")) + .then_output(|| DeleteContactListOutput::builder().build()); + + let mock_delete_email_template = mock!(Client::delete_email_template) + .match_requests(|req| req.template_name() == Some("weekly-coupons")) + .then_output(|| DeleteEmailTemplateOutput::builder().build()); + + let mock_delete_email_identity = mock!(Client::delete_email_identity) + .match_requests(|req| req.email_identity() == Some("test@example.com")) + .then_error(|| { + DeleteEmailIdentityError::NotFoundException(NotFoundException::builder().build()) + }); + + let client = mock_client!( + aws_sdk_sesv2, + RuleMode::Sequential, + &[ + &mock_delete_contact_list, + &mock_delete_email_template, + &mock_delete_email_identity, + ] + ); + + let mut workflow = SESWorkflow::new(client, &mut stdin, &mut stdout); + workflow.set_verified_email("test@example.com".into()); + + // Run the method + let result = workflow.cleanup().await; + + // Assert the output + let output = String::from_utf8(stdout)?; + assert!(output.contains("Contact list deleted successfully.")); + assert!(output.contains("Email template deleted successfully.")); + + assert!(result.is_err()); + assert!(format!("{result:?}").contains("Error deleting email identity:")); + + Ok(()) +} diff --git a/workflows/sesv2_weekly_mailer/README.md b/workflows/sesv2_weekly_mailer/README.md index 824c10e99db..ee53197b1da 100644 --- a/workflows/sesv2_weekly_mailer/README.md +++ b/workflows/sesv2_weekly_mailer/README.md @@ -22,6 +22,7 @@ This workflow demonstrates how to use the Amazon Simple Email Service (SES) v2 t 4. **Monitor and Review** - Review dashboards and metrics in the AWS console for the newsletter campaign. + - Because this workflow minimizes the number of email addresses necessary by utilizing subaddresses to show the features of SES Contact Lists, it is likely an email provider will mark the messages as Spam. Check your spam folder to ensure they sent. 5. **Clean up** @@ -34,7 +35,10 @@ This workflow demonstrates how to use the Amazon Simple Email Service (SES) v2 t Before running this workflow, ensure you have: - An AWS account with proper permissions to use Amazon SES v2. -- A verified email identity (domain or email address) in Amazon SES. +- (Optional) A verified email identity (domain or email address) in Amazon SES for the Sending address. + - This will be created during the workflow if it does not already exist. +- (Optional, if the account is in the SES Sandbox) A verified destination address. + - This will NOT be created during the workflow. ## AWS Services Used @@ -50,6 +54,9 @@ The workflow covers the following SES v2 API actions: - [`CreateContactList`](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_CreateContactList.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) +- [`DeleteContactList`](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_DeleteContactList.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) - [`ListContacts`](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_ListContacts.html) - [`SendEmail`](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html) (with both Simple and Template formats) diff --git a/workflows/sesv2_weekly_mailer/SPECIFICATION.md b/workflows/sesv2_weekly_mailer/SPECIFICATION.md index 137ba2c0815..2440a813e91 100644 --- a/workflows/sesv2_weekly_mailer/SPECIFICATION.md +++ b/workflows/sesv2_weekly_mailer/SPECIFICATION.md @@ -7,10 +7,34 @@ Use the Amazon Simple Email Service (SES) v2 API to manage a subscription list f 1. Create an email identity. - Request a `verified email` address from the user. This will be used as the `from` address, and the user will need to click a verification link in their email before continuing to part 3. - Operation: **CreateEmailIdentity** - - `EmailIdentity`: Value of the `verified email` given by the user. + - Parameters: + - `EmailIdentity`: Value of the `verified email` given by the user. + - Errors: + - `AlreadyExistsException`: If the identity already exists, skip this step and proceed with the next operation. This error can be safely ignored. + - `NotFoundException`: If the identity does not exist, fail the workflow and inform the user that the provided email address is not verified. + - `LimitExceededException`: If the limit for email identities is exceeded, fail the workflow and inform the user that they have reached the limit for email identities. 2. Create a contact list with the name `weekly-coupons-newsletter`. - Operation: **CreateContactList** - - `ContactListName`: `weekly-coupons-newsletter` + - Parameters: + - `ContactListName`: `weekly-coupons-newsletter` + - Errors: + - `AlreadyExistsException`: If the contact list already exists, skip this step and proceed with the next operation. This error can be safely ignored. + - `LimitExceededException`: If the limit for contact lists is exceeded, fail the workflow and inform the user that they have reached the limit for contact lists. +3. Create an email template named `weekly-coupons` with the following content: + - Subject: `Weekly Coupons Newsletter` + - HTML Content: Available in the `coupon-newsletter.html` file. + - Text Content: Available in the `coupon-newsletter.txt` file. + - The emails should include an [Unsubscribe](#) link, using the url `{{amazonSESUnsubscribeUrl}}`. + - Operation: **CreateEmailTemplate** + - Parameters: + - `TemplateName`: `weekly-coupons` + - `TemplateContent`: + - `Subject`: `Weekly Coupons Newsletter` + - `Html`: Read from the `coupon-newsletter.html` file + - `Text`: Read from the `coupon-newsletter.txt` file + - Errors: + - `AlreadyExistsException`: If the template already exists, skip this step and proceed with the next operation. This error can be safely ignored. + - `LimitExceededException`: If the limit for email templates is exceeded, fail the workflow and inform the user that they have reached the limit for email templates. ## Gather Subscriber Email Addresses @@ -21,32 +45,32 @@ Use the Amazon Simple Email Service (SES) v2 API to manage a subscription list f 2. For each email address created: 1. Create a new contact with the provided email address in the `weekly-coupons-newsletter` contact list. - Operation: **CreateContact** - - `ContactListName`: `weekly-coupons-newsletter` - - `EmailAddress`: The email address provided by the user. + - Parameters: + - `ContactListName`: `weekly-coupons-newsletter` + - `EmailAddress`: The email address provided by the user. + - Errors: + - `AlreadyExistsException`: If the contact already exists, skip this step for that contact and proceed with the next contact. This error can be safely ignored. 2. Send a welcome email to the new contact using the content from the `welcome.html` file. - Operation: **SendEmail** - - `FromEmailAddress`: Retrieve the value from the `VERIFIED_EMAIL_ADDRESS` environment variable. - - `Destination.ToAddresses`: The email address provided by the user. - - `Content.Simple.Subject.Data`: "Welcome to the Weekly Coupons Newsletter" - - `Content.Simple.Body.Text.Data`: Read the content from the `welcome.txt` file. - - `Content.Simple.Body.Html.Data`: Read the content from the `welcome.html` file. + - Parameters: + - `FromEmailAddress`: Use the `verified_email` address provided in Prepare the Application. + - `Destination.ToAddresses`: The generated email address variant. + - `Content.Simple.Subject.Data`: "Welcome to the Weekly Coupons Newsletter" + - `Content.Simple.Body.Text.Data`: Read the content from the `welcome.txt` file. + - `Content.Simple.Body.Html.Data`: Read the content from the `welcome.html` file. + - Errors: + - See Errors in `SendEmail` for "Send the Coupon Newsletter" + - Timing: + - Because the account is likely in sandbox, wait 2 seconds between sending emails. ## Send the Coupon Newsletter -1. Create an email template named `weekly-coupons` with the following content: - - Subject: `Weekly Coupons Newsletter` - - HTML Content: Available in the `coupon-newsletter.html` file. - - Text Content: Available in the `coupon-newsletter.txt` file. - - The emails should include an [Unsubscribe](#) link, using the url `{{amazonSESUnsubscribeUrl}}`. - - Operation: **CreateEmailTemplate** - - `TemplateName`: `weekly-coupons` - - `TemplateContent`: - - `Subject`: `Weekly Coupons Newsletter` - - `HtmlPart`: Read from the `coupon-newsletter.html` file - - `TextPart`: Read from the `coupon-newsletter.txt` file 2. Retrieve the list of contacts from the `weekly-coupons-newsletter` contact list. - Operation: **ListContacts** - - `ContactListName`: `weekly-coupons-newsletter` + - Parameters: + - `ContactListName`: `weekly-coupons-newsletter` + - Errors: + - `NotFoundException`: If the contact list does not exist, fail the workflow and inform the user that the contact list is missing. 3. Send an email using the `weekly-coupons` template to each contact in the list. - The email should include the following coupon items: 1. 20% off on all electronics @@ -56,33 +80,55 @@ Use the Amazon Simple Email Service (SES) v2 API to manage a subscription list f 5. 25% off on outdoor gear 6. 10% off on groceries - Operation: **SendEmail** - - `Destination`: - - `ToAddresses`: One email address from the `ListContacts` response (each email address must get a unique `SendEmail` call for tracking and unsubscribe purposes). - - `Content`: - - `Template`: - - `TemplateName`: `weekly-coupons` - - `TemplateData`: JSON string representing an object with one key, `coupons`, which is an array of coupon items. Each coupon entry in the array should have one key, `details`, with the details of the coupon. See `sample_coupons.json`. - - `FromEmailAddress`: (Use the verified email address from step 1) - - `ListManagementOptions`: - - `ContactListName`: `weekly-coupons-newsletter` to correctly populate Unsubscribe headers and the `{{amazonSESUnsubscribeUrl}}` value. + - Parameters: + - `Destination`: + - `ToAddresses`: One email address from the `ListContacts` response (each email address must get a unique `SendEmail` call for tracking and unsubscribe purposes). + - `Content`: + - `Template`: + - `TemplateName`: `weekly-coupons` + - `TemplateData`: JSON string representing an object with one key, `coupons`, which is an array of coupon items. Each coupon entry in the array should have one key, `details`, with the details of the coupon. See `sample_coupons.json`. + - `FromEmailAddress`: (Use the verified email address from step 1) + - `ListManagementOptions`: + - `ContactListName`: `weekly-coupons-newsletter` to correctly populate Unsubscribe headers and the `{{amazonSESUnsubscribeUrl}}` value. + - Errors: + - `AccountSuspendedException`: If the account is suspended, fail the workflow and inform the user that their account is suspended. + - `MailFromDomainNotVerifiedException`: If the sending domain is not verified, fail the workflow and inform the user that the sending domain is not verified. + - `MessageRejected`: If the message is rejected due to invalid content, fail the workflow and inform the user that the message content is invalid. + - `SendingPausedException`: If sending is paused, fail the workflow and inform the user that sending is currently paused for their account. + - Timing: + - Because the account is likely in sandbox, wait 2 seconds between sending emails. For more information on using templates with SES v2, refer to the [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/send-personalized-email-api.html). ## Monitor and Review 1. [Monitor your sending activity](https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity.html) using the [SES Homepage](https://console.aws.amazon.com/ses/home#/account) in the AWS console. +2. Wait for the user to press a key before continuing. ## Clean up 1. Delete the contact list. This operation also deletes all contacts in the list, without needing separate calls. - Operation: **DeleteContactList** - - `ContactListName`: `weekly-coupons-newsletter` + - Parameters: + - `ContactListName`: `weekly-coupons-newsletter` + - `NotFoundException`: If the contact list does not exist, skip this step and proceed with the next operation. This error can be safely ignored. + - Errors: + +- `NotFoundException`: If the contact list does not exist, skip this step and proceed with the next operation. This error can be safely ignored. + 2. Delete the template. - Operation: **DeleteEmailTemplate** - - `TemplateName`: `weekly-coupons` + - Parameters: + - `TemplateName`: `weekly-coupons` + - `NotFoundException`: If the email template does not exist, skip this step and proceed with the next operation. This error can be safely ignored. + - Errors: + - `NotFoundException`: If the email template does not exist, skip this step and proceed with the next operation. This error can be safely ignored. 3. Delete the email identity (optional). Ask the user before performing this step, as they may not want to re-verify the email identity. - Operation: **DeleteEmailIdentity** - - `EmailIdentity`: Value of the `verified email` given by the user in part 1. + - Parameters: + - `EmailIdentity`: Value of the `verified email` given by the user in part 1. + - Errors: + - `NotFoundException`: If the email identity does not exist, skip this step and proceed with the next operation. This error can be safely ignored. --- diff --git a/workflows/sesv2_weekly_mailer/coupon-newsletter.html b/workflows/sesv2_weekly_mailer/resources/coupon-newsletter.html similarity index 100% rename from workflows/sesv2_weekly_mailer/coupon-newsletter.html rename to workflows/sesv2_weekly_mailer/resources/coupon-newsletter.html diff --git a/workflows/sesv2_weekly_mailer/coupon-newsletter.txt b/workflows/sesv2_weekly_mailer/resources/coupon-newsletter.txt similarity index 100% rename from workflows/sesv2_weekly_mailer/coupon-newsletter.txt rename to workflows/sesv2_weekly_mailer/resources/coupon-newsletter.txt diff --git a/workflows/sesv2_weekly_mailer/sample_coupons.json b/workflows/sesv2_weekly_mailer/resources/sample_coupons.json similarity index 100% rename from workflows/sesv2_weekly_mailer/sample_coupons.json rename to workflows/sesv2_weekly_mailer/resources/sample_coupons.json diff --git a/workflows/sesv2_weekly_mailer/welcome.html b/workflows/sesv2_weekly_mailer/resources/welcome.html similarity index 100% rename from workflows/sesv2_weekly_mailer/welcome.html rename to workflows/sesv2_weekly_mailer/resources/welcome.html diff --git a/workflows/sesv2_weekly_mailer/welcome.txt b/workflows/sesv2_weekly_mailer/resources/welcome.txt similarity index 100% rename from workflows/sesv2_weekly_mailer/welcome.txt rename to workflows/sesv2_weekly_mailer/resources/welcome.txt From 05ff71932933808d79da9e89fac49c1e9b7b90b9 Mon Sep 17 00:00:00 2001 From: David Souther Date: Fri, 29 Mar 2024 10:25:15 -0400 Subject: [PATCH 02/13] Handle missing runtime files and update for Python review --- .../com/example/sesv2/NewsletterWorkflow.java | 12 ++- python/example_code/sesv2/newsletter.py | 25 +++--- python/example_code/sesv2/newsletter_test.py | 79 ++++++++++--------- rustv1/examples/ses/src/newsletter.rs | 27 ++++--- 4 files changed, 83 insertions(+), 60 deletions(-) diff --git a/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java b/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java index 8da8989820d..2a09faae477 100644 --- a/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java +++ b/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java @@ -133,8 +133,8 @@ public void prepareApplication() throws IOException { // snippet-start:[sesv2.java2.newsletter.CreateEmailTemplate] try { // Create an email template named "weekly-coupons" - String newsletterHtml = Files.readString(Paths.get("resources/coupon_newsletter/coupon-newsletter.html")); - String newsletterText = Files.readString(Paths.get("resources/coupon_newsletter/coupon-newsletter.txt")); + String newsletterHtml = loadFile("resources/coupon_newsletter/coupon-newsletter.html"); + String newsletterText = loadFile("resources/coupon_newsletter/coupon-newsletter.txt"); CreateEmailTemplateRequest templateRequest = CreateEmailTemplateRequest.builder() .templateName(TEMPLATE_NAME) @@ -164,6 +164,14 @@ public void prepareApplication() throws IOException { // snippet-end:[sesv2.java2.newsletter.CreateEmailTemplate] } + private String loadFile(String path) { + try { + return Files.readString(Paths.get(path)); + } catch (IOException ioe) { + return "Missing " + path; + } + } + /** * Helper method to create subscriber subaddresses. * diff --git a/python/example_code/sesv2/newsletter.py b/python/example_code/sesv2/newsletter.py index e116b17cfe1..5423c846e68 100644 --- a/python/example_code/sesv2/newsletter.py +++ b/python/example_code/sesv2/newsletter.py @@ -32,9 +32,12 @@ def load_file_content(file_path): Returns: str: The content of the file. """ - with open(file_path, "r") as file: - content = file.read() - return content + try: + with open(file_path, "r") as file: + content = file.read() + return content + except Exception: + return f"Missing {file_path}" def print_error(error): @@ -209,7 +212,7 @@ def send_coupon_newsletter(self): email_address = contact["EmailAddress"] try: # snippet-start:[python.example_code.sesv2.SendEmail.template] - send = self.ses_client.send_email( + self.ses_client.send_email( FromEmailAddress=self.verified_email, Destination={"ToAddresses": [email_address]}, Content={ @@ -222,7 +225,6 @@ def send_coupon_newsletter(self): ) # snippet-end:[python.example_code.sesv2.SendEmail.template] print(f"Newsletter sent to '{email_address}'.") - print("Debug: ", send) if self.sleep: # 1 email per second in sandbox mode, remove in production. sleep(1.1) @@ -234,12 +236,15 @@ def monitor_and_review(self): Provides instructions for monitoring sending activity in the AWS console. """ print( - "To monitor your sending activity, please visit the SES Homepage in the AWS console:" - ) - print("https://console.aws.amazon.com/ses/home#/account") - print( - "From there, you can view various dashboards and metrics related to your newsletter campaign." + """ +To monitor your sending activity, please visit the SES Homepage in the AWS console: + +https://console.aws.amazon.com/ses/home#/account + +From there, you can view various dashboards and metrics related to your newsletter campaign. +""" ) + input("Press enter to continue.") def clean_up(self): diff --git a/python/example_code/sesv2/newsletter_test.py b/python/example_code/sesv2/newsletter_test.py index 7ed9c47c703..10633b9a41e 100644 --- a/python/example_code/sesv2/newsletter_test.py +++ b/python/example_code/sesv2/newsletter_test.py @@ -2,13 +2,13 @@ # SPDX-License-Identifier: Apache-2.0 import json -import unittest import sys from botocore.exceptions import ClientError from io import StringIO +import pytest from unittest.mock import patch -from python.example_code.sesv2.newsletter import ( +from newsletter import ( SESv2Workflow, get_subaddress_variants, CONTACT_LIST_NAME, @@ -18,9 +18,9 @@ # Run tests with `python -m unittest` -class TestSESv2WorkflowPrepareApplication(unittest.TestCase): - @patch("main.boto3.client") - def setUp(self, mock_client): +class TestSESv2WorkflowPrepareApplication: + @patch("boto3.client") + def setup_method(self, method, mock_client): self.ses_client = mock_client() self.workflow = SESv2Workflow(self.ses_client) @@ -36,7 +36,7 @@ def test_prepare_application_success(self, mock_input): sys.stdout = sys.__stdout__ # Reset standard output expected_output = "Email identity 'verified@example.com' created successfully.\nContact list 'weekly-coupons-newsletter' created successfully.\nEmail template 'weekly-coupons' created successfully.\n" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output @patch("builtins.input", return_value="verified@example.com") def test_prepare_application_error_identity_already_exists(self, mock_input): @@ -52,7 +52,7 @@ def test_prepare_application_error_identity_already_exists(self, mock_input): sys.stdout = sys.__stdout__ expected_output = "Email identity 'verified@example.com' already exists.\nContact list 'weekly-coupons-newsletter' created successfully.\nEmail template 'weekly-coupons' created successfully.\n" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output @patch("builtins.input", return_value="invalid@example.com") def test_prepare_application_error_identity_not_found(self, mock_input): @@ -64,12 +64,12 @@ def test_prepare_application_error_identity_not_found(self, mock_input): captured_output = StringIO() sys.stdout = captured_output - with self.assertRaises(ClientError): + with pytest.raises(ClientError): self.workflow.prepare_application() sys.stdout = sys.__stdout__ expected_output = "" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output @patch("builtins.input", return_value="verified@example.com") def test_prepare_application_error_identity_limit_exceeded(self, mock_input): @@ -81,12 +81,12 @@ def test_prepare_application_error_identity_limit_exceeded(self, mock_input): captured_output = StringIO() sys.stdout = captured_output - with self.assertRaises(ClientError): + with pytest.raises(ClientError): self.workflow.prepare_application() sys.stdout = sys.__stdout__ expected_output = "" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output @patch("builtins.input", return_value="verified@example.com") def test_prepare_application_error_contact_list_limit_exceeded(self, mock_input): @@ -98,21 +98,21 @@ def test_prepare_application_error_contact_list_limit_exceeded(self, mock_input) captured_output = StringIO() sys.stdout = captured_output - with self.assertRaises(ClientError): + with pytest.raises(ClientError): self.workflow.prepare_application() sys.stdout = sys.__stdout__ expected_output = ( "Email identity 'verified@example.com' created successfully.\n" ) - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output -class TestSESv2WorkflowGatherSubscribers(unittest.TestCase): - @patch("main.boto3.client") - def setUp(self, mock_client): +class TestSESv2WorkflowGatherSubscribers: + @patch("boto3.client") + def setup_method(self, method, mock_client): self.ses_client = mock_client() - self.workflow = SESv2Workflow(self.ses_client, sleep=False) + self.workflow = SESv2Workflow(self.ses_client) self.workflow.verified_email = "verified@example.com" # Tests for gather_subscriber_email_addresses @@ -136,7 +136,7 @@ def test_gather_subscriber_email_addresses_success(self, mock_input): ) + "\n" ) - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output @patch("builtins.input", return_value="user@example.com") def test_gather_subscriber_email_addresses_error_contact_exists(self, mock_input): @@ -157,7 +157,7 @@ def test_gather_subscriber_email_addresses_error_contact_exists(self, mock_input email_variants = get_subaddress_variants("user@example.com", 3) expected_output = f"Contact with email '{email_variants[0]}' created successfully.\nWelcome email sent to '{email_variants[0]}'.\nContact with email '{email_variants[1]}' created successfully.\nWelcome email sent to '{email_variants[1]}'.\nContact with email '{email_variants[2]}' already exists. Skipping...\n" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output @patch("builtins.input", return_value="user@example.com") def test_gather_subscriber_email_addresses_error_send_email_failed( @@ -175,25 +175,25 @@ def test_gather_subscriber_email_addresses_error_send_email_failed( captured_output = StringIO() sys.stdout = captured_output - with self.assertRaises(ClientError): + with pytest.raises(ClientError): self.workflow.gather_subscriber_email_addresses() sys.stdout = sys.__stdout__ email_variants = get_subaddress_variants("user@example.com", 3) expected_output = f"Contact with email '{email_variants[0]}' created successfully.\nWelcome email sent to '{email_variants[0]}'.\nContact with email '{email_variants[1]}' created successfully.\n" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output -class TestSESv2WorkflowSendCouponNewsletter(unittest.TestCase): - @patch("main.boto3.client") - def setUp(self, mock_client): +class TestSESv2WorkflowSendCouponNewsletter: + @patch("boto3.client") + def setup_method(self, method, mock_client): self.ses_client = mock_client() - self.workflow = SESv2Workflow(self.ses_client, sleep=False) + self.workflow = SESv2Workflow(self.ses_client) self.workflow.verified_email = "verified@example.com" # Tests for send_coupon_newsletter @patch( - "main.load_file_content", + "newsletter.load_file_content", side_effect=[ "Template Content", "Plain Text Template Content", @@ -216,7 +216,7 @@ def test_send_coupon_newsletter_success(self, mock_load_file_content): sys.stdout = sys.__stdout__ expected_output = "Newsletter sent to 'user+1@example.com'.\nNewsletter sent to 'user+2@example.com'.\nNewsletter sent to 'user+3@example.com'.\n" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output def test_send_coupon_newsletter_error_contact_list_not_found(self): self.ses_client.list_contacts.side_effect = ClientError( @@ -230,9 +230,12 @@ def test_send_coupon_newsletter_error_contact_list_not_found(self): sys.stdout = sys.__stdout__ expected_output = f"Contact list '{CONTACT_LIST_NAME}' does not exist.\n" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output - @patch("main.load_file_content", return_value=json.dumps(["Coupon 1", "Coupon 2"])) + @patch( + "newsletter.load_file_content", + return_value=json.dumps(["Coupon 1", "Coupon 2"]), + ) def test_send_coupon_newsletter_error_send_email_failed( self, mock_load_file_content ): @@ -258,14 +261,14 @@ def test_send_coupon_newsletter_error_send_email_failed( sys.stdout = sys.__stdout__ expected_output = "Newsletter sent to 'user1@example.com'.\nError: An error occurred (MessageRejected) when calling the SendEmail operation: Unknown\nNewsletter sent to 'user3@example.com'.\n" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output -class TestSESv2WorkflowCleanUp(unittest.TestCase): - @patch("main.boto3.client") - def setUp(self, mock_client): +class TestSESv2WorkflowCleanUp: + @patch("boto3.client") + def setup_method(self, method, mock_client): self.ses_client = mock_client() - self.workflow = SESv2Workflow(self.ses_client, sleep=False) + self.workflow = SESv2Workflow(self.ses_client) self.workflow.verified_email = "verified@example.com" # Tests for clean_up @@ -281,7 +284,7 @@ def test_clean_up_success(self, mock_input): sys.stdout = sys.__stdout__ expected_output = f"Contact list '{CONTACT_LIST_NAME}' deleted successfully.\nEmail template '{TEMPLATE_NAME}' deleted successfully.\nSkipping email identity deletion.\n" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output @patch("builtins.input", return_value="n") def test_clean_up_error_contact_list_not_found(self, mock_input): @@ -298,7 +301,7 @@ def test_clean_up_error_contact_list_not_found(self, mock_input): sys.stdout = sys.__stdout__ expected_output = f"Contact list '{CONTACT_LIST_NAME}' does not exist.\nEmail template '{TEMPLATE_NAME}' deleted successfully.\nSkipping email identity deletion.\n" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output @patch("builtins.input", return_value="n") def test_clean_up_error_template_not_found(self, mock_input): @@ -315,7 +318,7 @@ def test_clean_up_error_template_not_found(self, mock_input): sys.stdout = sys.__stdout__ expected_output = f"Contact list '{CONTACT_LIST_NAME}' deleted successfully.\nEmail template '{TEMPLATE_NAME}' does not exist.\nSkipping email identity deletion.\n" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output @patch("builtins.input", return_value="y") def test_clean_up_error_identity_not_found(self, mock_input): @@ -332,4 +335,4 @@ def test_clean_up_error_identity_not_found(self, mock_input): sys.stdout = sys.__stdout__ expected_output = f"Contact list '{CONTACT_LIST_NAME}' deleted successfully.\nEmail template '{TEMPLATE_NAME}' deleted successfully.\nEmail identity '{self.workflow.verified_email}' does not exist.\n" - self.assertEqual(captured_output.getvalue(), expected_output) + assert captured_output.getvalue() == expected_output diff --git a/rustv1/examples/ses/src/newsletter.rs b/rustv1/examples/ses/src/newsletter.rs index fe82a144f8c..6006a7edb88 100644 --- a/rustv1/examples/ses/src/newsletter.rs +++ b/rustv1/examples/ses/src/newsletter.rs @@ -92,14 +92,18 @@ impl<'a> SESWorkflow<'a> { // snippet-end:[sesv2.rust.create-contact-list] // snippet-start:[sesv2.rust.create-email-template] - let TEMPLATE_HTML: &str = include_str!("../resources/newsletter/coupon-newsletter.html"); - let TEMPLATE_TEXT: &str = include_str!("../resources/newsletter/coupon-newsletter.txt"); + let template_html = + std::fs::read_to_string("../resources/newsletter/coupon-newsletter.html") + .unwrap_or_else(|_| "Missing coupon-newsletter.html".to_string()); + let template_text = + std::fs::read_to_string("../resources/newsletter/coupon-newsletter.txt") + .unwrap_or_else(|_| "Missing coupon-newsletter.txt".to_string()); // Create the email template let template_content = EmailTemplateContent::builder() .subject("Weekly Coupons Newsletter") - .html(TEMPLATE_HTML) - .text(TEMPLATE_TEXT) + .html(template_html) + .text(template_text) .build(); match self @@ -176,8 +180,10 @@ impl<'a> SESWorkflow<'a> { // Send the welcome email // snippet-start:[sesv2.rust.send-email.simple] - let WELCOME_HTML: &str = include_str!("../resources/newsletter/welcome.html"); - let WELCOME_TXT: &str = include_str!("../resources/newsletter/welcome.txt"); + let welcome_html = std::fs::read_to_string("../resources/newsletter/welcome.html") + .unwrap_or_else(|_| "Missing welcome.html".to_string()); + let welcome_txt = std::fs::read_to_string("../resources/newsletter/welcome.txt") + .unwrap_or_else(|_| "Missing welcome.txt".to_string()); let email_content = EmailContent::builder() .simple( Message::builder() @@ -188,8 +194,8 @@ impl<'a> SESWorkflow<'a> { ) .body( Body::builder() - .html(Content::builder().data(WELCOME_HTML).build()?) - .text(Content::builder().data(WELCOME_TXT).build()?) + .html(Content::builder().data(welcome_html).build()?) + .text(Content::builder().data(welcome_txt).build()?) .build(), ) .build(), @@ -253,12 +259,13 @@ impl<'a> SESWorkflow<'a> { let email = email.email_address.unwrap(); // snippet-start:[sesv2.rust.send-email.template] - let COUPONS: &str = include_str!("../resources/newsletter/sample_coupons.json"); + let coupons = std::fs::read_to_string("../resources/newsletter/sample_coupons.json") + .unwrap_or_else(|_| r#"{"coupons":[]}"#.to_string()); let email_content = EmailContent::builder() .template( Template::builder() .template_name(TEMPLATE_NAME) - .template_data(COUPONS) + .template_data(coupons) .build(), ) .build(); From 7d1bdc255fd906c8586ae1f6c40ba268abaafec0 Mon Sep 17 00:00:00 2001 From: David Souther Date: Sat, 30 Mar 2024 11:16:10 -0400 Subject: [PATCH 03/13] Cross-link READMEs --- javav2/example_code/ses/Readme.md | 4 ++++ python/example_code/sesv2/README.md | 4 ++++ rustv1/examples/ses/README.md | 4 ++++ workflows/sesv2_weekly_mailer/README.md | 4 +++- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/javav2/example_code/ses/Readme.md b/javav2/example_code/ses/Readme.md index 392d37d5f73..8c55738c693 100644 --- a/javav2/example_code/ses/Readme.md +++ b/javav2/example_code/ses/Readme.md @@ -52,6 +52,10 @@ Code excerpts that show you how to call individual service functions. +#### SESv2 Newsletter Workflow + +Review the usage instructions in [`workflows/sesv2_weekly_mailer/README.md`](../../../workflows/sesv2_weekly_mailer/README.md). + To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer/resources into a new folder, javav2/example_code/ses/resources/coupon_newsletter. diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index 603b815c4ec..7b9d9f13c03 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -58,6 +58,10 @@ Code excerpts that show you how to call individual service functions. +#### SESv2 Newsletter Workflow + +Review the usage instructions in [`workflows/sesv2_weekly_mailer/README.md`](../../../workflows/sesv2_weekly_mailer/README.md). + To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer/resources into this folder. diff --git a/rustv1/examples/ses/README.md b/rustv1/examples/ses/README.md index da58ad0ce84..c34b52fc1d5 100644 --- a/rustv1/examples/ses/README.md +++ b/rustv1/examples/ses/README.md @@ -54,6 +54,10 @@ Code excerpts that show you how to call individual service functions. +#### SESv2 Newsletter Workflow + +Review the usage instructions in [`workflows/sesv2_weekly_mailer/README.md`](../../../workflows/sesv2_weekly_mailer/README.md). + To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer/resources into a new folder, rustv1/examples/ses/resources/newsletter. diff --git a/workflows/sesv2_weekly_mailer/README.md b/workflows/sesv2_weekly_mailer/README.md index ee53197b1da..1123464bff3 100644 --- a/workflows/sesv2_weekly_mailer/README.md +++ b/workflows/sesv2_weekly_mailer/README.md @@ -64,7 +64,9 @@ The workflow covers the following SES v2 API actions: This example is implemented in the following languages: -- [Python](../../python/example_code/sesv2/scenarios/wkflw-sesv2-mailer/README.md) +- [Java](../../javav2/example_code/ses/README.md) +- [Python](../../python/example_code/sesv2/README.md) +- [Rust](../../rustv1/examples/ses/README.md) --- From b69dd38603156fc0dd4edb99cf0d797f4003a066 Mon Sep 17 00:00:00 2001 From: David Souther Date: Mon, 1 Apr 2024 10:56:12 -0400 Subject: [PATCH 04/13] READMEs --- javav2/example_code/ses/Readme.md | 44 +++++++++++++++-------------- python/example_code/sesv2/README.md | 37 ++++++++++++++---------- rustv1/examples/ses/README.md | 26 +++++++++++------ 3 files changed, 62 insertions(+), 45 deletions(-) diff --git a/javav2/example_code/ses/Readme.md b/javav2/example_code/ses/Readme.md index 8c55738c693..15786a59f73 100644 --- a/javav2/example_code/ses/Readme.md +++ b/javav2/example_code/ses/Readme.md @@ -1,20 +1,20 @@ -# Amazon SES v2 API code examples for the SDK for Java 2.x +# Amazon SES code examples for the SDK for Java 2.x ## Overview -Shows how to use the AWS SDK for Java 2.x to work with Amazon Simple Email Service v2 API. +Shows how to use the AWS SDK for Java 2.x to work with Amazon Simple Email Service (Amazon SES). -_Amazon SES v2 API is a reliable, scalable, and cost-effective email service._ +_Amazon SES is a reliable, scalable, and cost-effective email service._ ## ⚠ Important -- Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). -- Running the tests might result in charges to your AWS account. -- We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). -- This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). @@ -25,6 +25,7 @@ _Amazon SES v2 API is a reliable, scalable, and cost-effective email service._ For prerequisites, see the [README](../../README.md#Prerequisites) in the `javav2` folder. + @@ -32,16 +33,11 @@ For prerequisites, see the [README](../../README.md#Prerequisites) in the `javav Code excerpts that show you how to call individual service functions. -- [Create a contact in a contact list](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L199) (`CreateContact`) -- [Create a contact list](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L113) (`CreateContactList`) -- [Create an email identity](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L91) (`CreateEmailIdentity`) -- [Create an email template](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L133) (`CreateEmailTemplate`) -- [Delete a contact list](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L325) (`DeleteContactList`) -- [Delete an email identity](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L369) (`DeleteEmailIdentity`) -- [Delete an email template](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L344) (`DeleteEmailTemplate`) -- [List the contacts in a contact list](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L252) (`ListContacts`) -- [Send a simple email](src/main/java/com/example/sesv2/SendEmail.java#L6) (`SendEmail`) -- [Send a templated email](src/main/java/com/example/sesv2/NewsletterWorkflow.java#L263) (`SendEmail`) +- [List email templates](src/main/java/com/example/sesv2/ListTemplates.java#L6) (`ListTemplates`) +- [List identities](src/main/java/com/example/ses/ListIdentities.java#L6) (`ListIdentities`) +- [Send email](src/main/java/com/example/ses/SendMessageEmailRequest.java#L6) (`SendEmail`) +- [Send templated email](src/main/java/com/example/sesv2/SendEmailTemplate.java#L6) (`SendTemplatedEmail`) + @@ -50,6 +46,7 @@ Code excerpts that show you how to call individual service functions. ### Instructions + #### SESv2 Newsletter Workflow @@ -60,21 +57,26 @@ To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer + + ### Tests ⚠ Running tests might result in charges to your AWS account. + To find instructions for running these tests, see the [README](../../README.md#Tests) in the `javav2` folder. + + ## Additional resources -- [Amazon SES v2 API Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/Welcome.html) -- [Amazon SES v2 API API Reference](https://docs.aws.amazon.com/ses/latest/APIReference-V2/Welcome.html) -- [SDK for Java 2.x Amazon SES v2 API reference](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/ses/package-summary.html) +- [Amazon SES Developer Guide](https://docs.aws.amazon.com/ses/latest/dg/Welcome.html) +- [Amazon SES API Reference](https://docs.aws.amazon.com/ses/latest/APIReference/Welcome.html) +- [SDK for Java 2.x Amazon SES reference](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/ses/package-summary.html) @@ -83,4 +85,4 @@ in the `javav2` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index 7b9d9f13c03..1f44afb5b67 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -11,10 +11,10 @@ _Amazon SES v2 API is a reliable, scalable, and cost-effective email service._ ## ⚠ Important -- Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). -- Running the tests might result in charges to your AWS account. -- We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). -- This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). @@ -38,16 +38,17 @@ python -m pip install -r requirements.txt Code excerpts that show you how to call individual service functions. -- [Create a contact in a contact list](newsletter.py#L149) (`CreateContact`) -- [Create a contact list](newsletter.py#L99) (`CreateContactList`) -- [Create an email identity](newsletter.py#L86) (`CreateEmailIdentity`) -- [Create an email template](newsletter.py#L112) (`CreateEmailTemplate`) -- [Delete a contact list](newsletter.py#L250) (`DeleteContactList`) -- [Delete an email identity](newsletter.py#L278) (`DeleteEmailIdentity`) -- [Delete an email template](newsletter.py#L263) (`DeleteEmailTemplate`) -- [List the contacts in a contact list](newsletter.py#L192) (`ListContacts`) -- [Send a simple email](newsletter.py#L158) (`SendEmail`) -- [Send a templated email](newsletter.py#L211) (`SendEmail`) +- [Create a contact in a contact list](newsletter.py#L152) (`CreateContact`) +- [Create a contact list](newsletter.py#L102) (`CreateContactList`) +- [Create an email identity](newsletter.py#L89) (`CreateEmailIdentity`) +- [Create an email template](newsletter.py#L115) (`CreateEmailTemplate`) +- [Delete a contact list](newsletter.py#L255) (`DeleteContactList`) +- [Delete an email identity](newsletter.py#L283) (`DeleteEmailIdentity`) +- [Delete an email template](newsletter.py#L268) (`DeleteEmailTemplate`) +- [List the contacts in a contact list](newsletter.py#L195) (`ListContacts`) +- [Send a simple email](newsletter.py#L161) (`SendEmail`) +- [Send a templated email](newsletter.py#L214) (`SendEmail`) + @@ -56,6 +57,7 @@ Code excerpts that show you how to call individual service functions. ### Instructions + #### SESv2 Newsletter Workflow @@ -66,13 +68,18 @@ To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer + + ### Tests ⚠ Running tests might result in charges to your AWS account. + To find instructions for running these tests, see the [README](../../README.md#Tests) in the `python` folder. + + @@ -89,4 +96,4 @@ in the `python` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/rustv1/examples/ses/README.md b/rustv1/examples/ses/README.md index c34b52fc1d5..149f02ceb5a 100644 --- a/rustv1/examples/ses/README.md +++ b/rustv1/examples/ses/README.md @@ -11,10 +11,10 @@ _Amazon SES v2 API is a reliable, scalable, and cost-effective email service._ ## ⚠ Important -- Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). -- Running the tests might result in charges to your AWS account. -- We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). -- This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). @@ -25,6 +25,7 @@ _Amazon SES v2 API is a reliable, scalable, and cost-effective email service._ For prerequisites, see the [README](../../README.md#Prerequisites) in the `rustv1` folder. + @@ -36,14 +37,15 @@ Code excerpts that show you how to call individual service functions. - [Create a contact list](src/bin/create-contact-list.rs#L26) (`CreateContactList`) - [Create an email identity](src/newsletter.rs#L49) (`CreateEmailIdentity`) - [Create an email template](src/newsletter.rs#L94) (`CreateEmailTemplate`) -- [Delete a contact list](src/newsletter.rs#L338) (`DeleteContactList`) -- [Delete an email identity](src/newsletter.rs#L376) (`DeleteEmailIdentity`) -- [Delete an email template](src/newsletter.rs#L351) (`DeleteEmailTemplate`) +- [Delete a contact list](src/newsletter.rs#L345) (`DeleteContactList`) +- [Delete an email identity](src/newsletter.rs#L383) (`DeleteEmailIdentity`) +- [Delete an email template](src/newsletter.rs#L358) (`DeleteEmailTemplate`) - [Get identity information](src/bin/is-email-verified.rs#L26) (`GetEmailIdentity`) - [List the contact lists](src/bin/list-contact-lists.rs#L22) (`ListContactLists`) - [List the contacts in a contact list](src/bin/list-contacts.rs#L26) (`ListContacts`) - [Send a simple email](src/bin/send-email.rs#L39) (`SendEmail`) -- [Send a templated email](src/newsletter.rs#L255) (`SendEmail`) +- [Send a templated email](src/newsletter.rs#L261) (`SendEmail`) + @@ -52,6 +54,7 @@ Code excerpts that show you how to call individual service functions. ### Instructions + #### SESv2 Newsletter Workflow @@ -62,13 +65,18 @@ To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer + + ### Tests ⚠ Running tests might result in charges to your AWS account. + To find instructions for running these tests, see the [README](../../README.md#Tests) in the `rustv1` folder. + + @@ -85,4 +93,4 @@ in the `rustv1` folder. Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: Apache-2.0 +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file From ac4216424240088624532816f97641c856697d15 Mon Sep 17 00:00:00 2001 From: David Souther Date: Wed, 3 Apr 2024 10:37:17 -0400 Subject: [PATCH 05/13] Updated metadata from review --- .doc_gen/metadata/sesv2_metadata.yaml | 81 ++++++++++++++++++++ .doc_gen/validation.yaml | 1 + python/example_code/sesv2/newsletter.py | 8 +- rustv1/examples/ses/src/bin/newsletter.rs | 3 + rustv1/examples/ses/src/lib.rs | 3 + rustv1/examples/ses/src/newsletter.rs | 3 + rustv1/examples/ses/tests/test_newsletter.rs | 3 + 7 files changed, 101 insertions(+), 1 deletion(-) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 0a2a0790926..4c0c18d7e19 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -28,6 +28,7 @@ sesv2_CreateContactList: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateContactList services: sesv2: {CreateContactList} @@ -60,6 +61,7 @@ sesv2_CreateContact: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateContact services: sesv2: {CreateContact} @@ -124,6 +126,7 @@ sesv2_ListContacts: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.ListContacts services: sesv2: {ListContacts} @@ -165,6 +168,7 @@ sesv2_SendEmail_Simple: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.simple services: sesv2: {SendEmail} @@ -197,6 +201,7 @@ sesv2_SendEmail_Template: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.template services: sesv2: {SendEmail} @@ -221,6 +226,7 @@ sesv2_CreateEmailIdentity: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailIdentity Rust: versions: @@ -253,6 +259,7 @@ sesv2_CreateEmailTemplate: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailTemplate Rust: versions: @@ -286,6 +293,7 @@ sesv2_DeleteContactList: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteContactList Rust: versions: @@ -319,6 +327,7 @@ sesv2_DeleteEmailIdentity: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailIdentity Rust: versions: @@ -352,6 +361,7 @@ sesv2_DeleteEmailTemplate: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailTemplate Rust: versions: @@ -363,3 +373,74 @@ sesv2_DeleteEmailTemplate: - sesv2.rust.delete-email-template services: sesv2: {DeleteEmailTemplate} + +sesv2_NewsletterWorkflow: + title: A complete &SESv2; Newsletter workflow using an &AWS; SDK + title_abbrev: Newsletter workflow + synopsis: "&SESv2; newsletter workflow." + category: + languages: + Java: + versions: + - sdk_version: 2 + github: javav2/example_code/ses + excerpts: + - description: + snippet_tags: + - sesv2.java2.newsletter.CreateContactList + - sesv2.java2.newsletter.CreateContact + - sesv2.java2.newsletter.ListContacts + - sesv2.java2.newsletter.SendEmail.template + - sesv2.java2.newsletter.CreateEmailIdentity + - sesv2.java2.newsletter.CreateEmailTemplate + - sesv2.java2.newsletter.DeleteContactList + - sesv2.java2.newsletter.DeleteEmailIdentity + - sesv2.java2.newsletter.DeleteEmailTemplate + Python: + versions: + - sdk_version: 3 + github: python/example_code/sesv2 + excerpts: + - description: + snippet_tags: + - python.example_code.sesv2.SESv2Workflow.decl + - python.example_code.sesv2.CreateContactList + - python.example_code.sesv2.CreateContact + - python.example_code.sesv2.ListContacts + - python.example_code.sesv2.SendEmail.simple + - python.example_code.sesv2.SendEmail.template + - python.example_code.sesv2.CreateEmailIdentity + - python.example_code.sesv2.CreateEmailTemplate + - python.example_code.sesv2.DeleteContactList + - python.example_code.sesv2.DeleteEmailIdentity + - python.example_code.sesv2.DeleteEmailTemplate + Rust: + versions: + - sdk_version: 1 + github: rustv1/examples/ses + excerpts: + - description: + snippet_tags: + - sesv2.rust.create-contact-list + - sesv2.rust.create-contact + - sesv2.rust.list-contacts + - sesv2.rust.send-email.template + - sesv2.rust.create-email-identity + - sesv2.rust.create-email-template + - sesv2.rust.delete-contact-list + - sesv2.rust.delete-email-identity + - sesv2.rust.delete-email-template + services: + sesv2: + { + CreateContactList, + CreateContact, + ListContacts, + SendEmail.simple, + SendEmail.template, + CreateEmailIdentity, + CreateEmailTemplate, + DeleteContactList, + DeleteEmailIdentity, + DeleteEmailTemplate, + } diff --git a/.doc_gen/validation.yaml b/.doc_gen/validation.yaml index 2c16f411d22..87da83ce0b2 100644 --- a/.doc_gen/validation.yaml +++ b/.doc_gen/validation.yaml @@ -156,6 +156,7 @@ allow_list: - "respondToDevicePasswordVerifierChallenge" - "role/AmazonBedrockExecutionRoleForAgents" - "role/AmazonSageMakerGeospatialFullAccess" + - "rustv1/examples/ses/resources/newsletter" - "s3/src/main/java/com/example/s3/ParseUri" - "s3_client_side_encryption_sym_master_key" - "serial/CORE_THING_NAME/write/dev/serial1" diff --git a/python/example_code/sesv2/newsletter.py b/python/example_code/sesv2/newsletter.py index 5423c846e68..30a0c166d22 100644 --- a/python/example_code/sesv2/newsletter.py +++ b/python/example_code/sesv2/newsletter.py @@ -69,6 +69,7 @@ def get_subaddress_variants(base_email, num_variants): return variants +# snippet-start:[python.example_code.sesv2.SESv2Workflow.decl] class SESv2Workflow: """ A class to manage the SES v2 Coupon Newsletter Workflow. @@ -78,6 +79,8 @@ def __init__(self, ses_client, sleep=True): self.ses_client = ses_client self.sleep = sleep + # snippet-end:[python.example_code.sesv2.SESv2Workflow.decl] + def prepare_application(self): """ Prepares the application by creating an email identity and a contact list. @@ -295,7 +298,7 @@ def clean_up(self): print("Skipping email identity deletion.") -# Main function +# snippet-start:[python.example_code.sesv2.SESv2Workflow.main] def main(): """ The main function that orchestrates the execution of the workflow. @@ -313,5 +316,8 @@ def main(): workflow.clean_up() +# snippet-end:[python.example_code.sesv2.SESv2Workflow.main] + + if __name__ == "__main__": main() diff --git a/rustv1/examples/ses/src/bin/newsletter.rs b/rustv1/examples/ses/src/bin/newsletter.rs index 5b850a8df40..eb865d6e1da 100644 --- a/rustv1/examples/ses/src/bin/newsletter.rs +++ b/rustv1/examples/ses/src/bin/newsletter.rs @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + use anyhow::{anyhow, Result}; use aws_sdk_sesv2::Client; use ses_code_examples::newsletter::SESWorkflow; diff --git a/rustv1/examples/ses/src/lib.rs b/rustv1/examples/ses/src/lib.rs index 688839d67e3..356ec39940d 100644 --- a/rustv1/examples/ses/src/lib.rs +++ b/rustv1/examples/ses/src/lib.rs @@ -1 +1,4 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + pub mod newsletter; diff --git a/rustv1/examples/ses/src/newsletter.rs b/rustv1/examples/ses/src/newsletter.rs index 6006a7edb88..44e66f5252f 100644 --- a/rustv1/examples/ses/src/newsletter.rs +++ b/rustv1/examples/ses/src/newsletter.rs @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + use anyhow::{anyhow, Result}; use aws_sdk_sesv2::{ types::{ diff --git a/rustv1/examples/ses/tests/test_newsletter.rs b/rustv1/examples/ses/tests/test_newsletter.rs index 5e0f98d0106..c430269b111 100644 --- a/rustv1/examples/ses/tests/test_newsletter.rs +++ b/rustv1/examples/ses/tests/test_newsletter.rs @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + use std::io::Cursor; use anyhow::Result; From 8e8c3d50116576a962a783f9c2219a14d8101096 Mon Sep 17 00:00:00 2001 From: David Souther Date: Wed, 3 Apr 2024 14:21:14 -0400 Subject: [PATCH 06/13] Massage tests and scanner structure --- .../com/example/sesv2/NewsletterWorkflow.java | 40 +++++----- .../example/sesv2/NewsletterWorkflowTest.java | 76 +++++++++---------- 2 files changed, 61 insertions(+), 55 deletions(-) diff --git a/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java b/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java index 2a09faae477..3b5d6a94bc8 100644 --- a/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java +++ b/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.Scanner; -import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.sesv2.SesV2Client; import software.amazon.awssdk.services.sesv2.model.*; @@ -20,7 +19,7 @@ * coupon newsletter to a list of contacts. */ public class NewsletterWorkflow { - private static final String CONTACT_LIST_NAME = "weekly-coupons-newsletter"; + public static final String CONTACT_LIST_NAME = "weekly-coupons-newsletter"; private static final String TEMPLATE_NAME = "weekly-coupons"; private static final String INTRO = """ Welcome to the Amazon SES v2 Coupon Newsletter Workflow! @@ -35,6 +34,7 @@ public class NewsletterWorkflow { """; private final SesV2Client sesClient; private String verifiedEmail = ""; + private NewsletterScanner scanner; public void test_setVerifiedEmail(String verifiedEmail) { this.verifiedEmail = verifiedEmail; @@ -46,8 +46,9 @@ public void test_setVerifiedEmail(String verifiedEmail) { * @param sesClient The SesV2Client instance to be used for interacting with the * SES v2 service. */ - public NewsletterWorkflow(SesV2Client sesClient) { + public NewsletterWorkflow(SesV2Client sesClient, NewsletterScanner scanner) { this.sesClient = sesClient; + this.scanner = scanner; } /** @@ -57,11 +58,8 @@ public NewsletterWorkflow(SesV2Client sesClient) { */ public static void main(String[] args) { System.out.println(INTRO); - SesV2Client sesClient = SesV2Client.builder() - .region(Region.AWS_GLOBAL) - .build(); - - new NewsletterWorkflow(sesClient).run(); + SesV2Client sesClient = SesV2Client.builder().build(); + new NewsletterWorkflow(sesClient, new NewsletterScanner()).run(); } /** @@ -84,9 +82,7 @@ public void run() { public void prepareApplication() throws IOException { // 1. Create an email identity System.out.println("Enter the verified email address: "); - Scanner scanner = new Scanner(System.in); - this.verifiedEmail = scanner.nextLine(); - scanner.close(); + verifiedEmail = scanner.nextLine(); // snippet-start:[sesv2.java2.newsletter.CreateEmailIdentity] try { @@ -197,10 +193,8 @@ private List createSubscriberSubaddresses(String baseEmail) { * subscriber. */ public void gatherSubscriberEmails() throws IOException { - Scanner scanner = new Scanner(System.in); System.out.print("Enter a base email address for subscribing to the newsletter: "); String baseEmail = scanner.nextLine(); - scanner.close(); for (String emailAddress : createSubscriberSubaddresses(baseEmail)) { // "weekly-coupons-newsletter" contact list @@ -321,9 +315,7 @@ public void monitorAndReview() { + "https://console.aws.amazon.com/ses/home#/account\n" + "For more detailed monitoring, refer to the SES Developer Guide:\n" + "https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity.html"); - Scanner scanner = new Scanner(System.in); scanner.nextLine(); - scanner.close(); } /** @@ -369,9 +361,7 @@ public void cleanUp() { // snippet-end:[sesv2.java2.newsletter.DeleteEmailTemplate] System.out.println("\nDo you want to delete the email identity? (y/n)"); - Scanner scanner = new Scanner(System.in); String input = scanner.nextLine(); - scanner.close(); if (input.equalsIgnoreCase("y")) { // snippet-start:[sesv2.java2.newsletter.DeleteEmailIdentity] @@ -397,3 +387,19 @@ public void cleanUp() { // snippet-end:[sesv2.java2.newsletter.DeleteEmailIdentity] } } + +class NewsletterScanner { + private Scanner scanner; + + NewsletterScanner() { + this(new Scanner(System.in)); + } + + NewsletterScanner(Scanner scanner) { + this.scanner = scanner; + } + + String nextLine() { + return scanner.nextLine(); + } +} \ No newline at end of file diff --git a/javav2/example_code/ses/src/test/java/com/example/sesv2/NewsletterWorkflowTest.java b/javav2/example_code/ses/src/test/java/com/example/sesv2/NewsletterWorkflowTest.java index 4b6f28f7968..c5d09aaab3b 100644 --- a/javav2/example_code/ses/src/test/java/com/example/sesv2/NewsletterWorkflowTest.java +++ b/javav2/example_code/ses/src/test/java/com/example/sesv2/NewsletterWorkflowTest.java @@ -18,6 +18,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; public class NewsletterWorkflowTest { @@ -30,10 +32,13 @@ public class NewsletterWorkflowTest { @Mock private SesV2Client sesClient; + @Mock + private NewsletterScanner scanner; + @Before public void openMocks() { closeable = MockitoAnnotations.openMocks(this); - scenario = new NewsletterWorkflow(sesClient); + scenario = new NewsletterWorkflow(sesClient, scanner); System.setOut(new PrintStream(outContent)); System.setErr(new PrintStream(errContent)); outContent.reset(); @@ -49,7 +54,8 @@ public void releaseMocks() throws Exception { @Test public void test_prepareApplication_success() throws IOException { // Mock the necessary AWS SDK calls and responses - String verifiedEmail = "test@example.com"; + when(scanner.nextLine()).thenReturn("test@example.com"); + CreateEmailIdentityResponse emailIdentityResponse = CreateEmailIdentityResponse.builder().build(); when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenReturn(emailIdentityResponse); @@ -59,8 +65,6 @@ public void test_prepareApplication_success() throws IOException { CreateEmailTemplateResponse createEmailTemplateResponse = CreateEmailTemplateResponse.builder().build(); when(sesClient.createEmailTemplate(any(CreateEmailTemplateRequest.class))).thenReturn(createEmailTemplateResponse); - System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); - scenario.prepareApplication(); assertThat(outContent.toString(), containsString("Email identity created: test@example.com")); @@ -70,14 +74,13 @@ public void test_prepareApplication_success() throws IOException { @Test public void test_prepareApplication_error_identityAlreadyExists() { // Mock the necessary AWS SDK calls and responses - String verifiedEmail = "test@example.com"; + when(scanner.nextLine()).thenReturn("test@example.com"); + when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenThrow(AlreadyExistsException.class); CreateContactListResponse contactListResponse = CreateContactListResponse.builder().build(); when(sesClient.createContactList(any(CreateContactListRequest.class))).thenReturn(contactListResponse); - System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); - try { scenario.prepareApplication(); } catch (Exception e) { @@ -91,10 +94,9 @@ public void test_prepareApplication_error_identityAlreadyExists() { @Test public void test_prepareApplication_error_identityNotFound() { // Mock the necessary AWS SDK calls and responses - String verifiedEmail = "test@example.com"; - when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenThrow(NotFoundException.class); + when(scanner.nextLine()).thenReturn("test@example.com"); - System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); + when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenThrow(NotFoundException.class); try { scenario.prepareApplication(); @@ -107,10 +109,9 @@ public void test_prepareApplication_error_identityNotFound() { @Test public void test_prepareApplication_error_identityLimitExceeded() { // Mock the necessary AWS SDK calls and responses - String verifiedEmail = "test@example.com"; - when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenThrow(LimitExceededException.class); + when(scanner.nextLine()).thenReturn("test@example.com"); - System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); + when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenThrow(LimitExceededException.class); try { scenario.prepareApplication(); @@ -124,14 +125,13 @@ public void test_prepareApplication_error_identityLimitExceeded() { @Test public void test_prepareApplication_error_contactListLimitExceeded() { // Mock the necessary AWS SDK calls and responses - String verifiedEmail = "test@example.com"; + when(scanner.nextLine()).thenReturn("test@example.com"); + CreateEmailIdentityResponse emailIdentityResponse = CreateEmailIdentityResponse.builder().build(); when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenReturn(emailIdentityResponse); when(sesClient.createContactList(any(CreateContactListRequest.class))).thenThrow(LimitExceededException.class); - System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); - try { scenario.prepareApplication(); } catch (Exception e) { @@ -144,7 +144,8 @@ public void test_prepareApplication_error_contactListLimitExceeded() { @Test public void test_prepareApplication_error_templateAlreadyExists() { // Mock the necessary AWS SDK calls and responses - String verifiedEmail = "test@example.com"; + when(scanner.nextLine()).thenReturn("test@example.com"); + CreateEmailIdentityResponse emailIdentityResponse = CreateEmailIdentityResponse.builder().build(); when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenReturn(emailIdentityResponse); @@ -153,8 +154,6 @@ public void test_prepareApplication_error_templateAlreadyExists() { when(sesClient.createEmailTemplate(any(CreateEmailTemplateRequest.class))).thenThrow(AlreadyExistsException.class); - System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); - try { scenario.prepareApplication(); } catch (Exception e) { @@ -167,9 +166,7 @@ public void test_prepareApplication_error_templateAlreadyExists() { @Test public void test_prepareApplication_error_templateLimitExceeded() { // Mock the necessary AWS SDK calls and responses - // Mock the necessary AWS SDK calls and responses - String verifiedEmail = "test@example.com"; - System.setIn(new ByteArrayInputStream(verifiedEmail.getBytes())); + when(scanner.nextLine()).thenReturn("test@example.com"); CreateEmailIdentityResponse emailIdentityResponse = CreateEmailIdentityResponse.builder().build(); when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenReturn(emailIdentityResponse); @@ -194,21 +191,19 @@ public void test_prepareApplication_error_templateLimitExceeded() { @Test public void test_gatherSubscriberEmails_success() throws IOException { // Mock the necessary AWS SDK calls and responses - String baseEmail = "user@example.com"; + when(scanner.nextLine()).thenReturn("user@example.com"); + CreateContactResponse contactResponse = CreateContactResponse.builder().build(); when(sesClient.createContact(any(CreateContactRequest.class))).thenReturn(contactResponse); SendEmailResponse welcomeEmailResponse = SendEmailResponse.builder().messageId("message-id").build(); when(sesClient.sendEmail(any(SendEmailRequest.class))).thenReturn(welcomeEmailResponse); - System.setIn(new ByteArrayInputStream(baseEmail.getBytes())); - scenario.gatherSubscriberEmails(); String output = outContent.toString(); for (int i = 1; i <= 3; i++) { - String expectedEmail = baseEmail + "+ses-weekly-newsletter-" + i + - "@example.com"; + String expectedEmail = "user+ses-weekly-newsletter-" + i + "@example.com"; assertThat(output, containsString("Contact created: " + expectedEmail)); assertThat(output, containsString("Welcome email sent: message-id")); } @@ -217,16 +212,21 @@ public void test_gatherSubscriberEmails_success() throws IOException { @Test public void test_gatherSubscriberEmails_error_contactAlreadyExists() { // Mock the necessary AWS SDK calls and responses - String baseEmail = "user@example.com"; + when(scanner.nextLine()).thenReturn("user@example.com"); + when(sesClient.createContact(any(CreateContactRequest.class))).thenThrow( AlreadyExistsException.class); + when(sesClient.createContact( + eq(CreateContactRequest.builder().contactListName(NewsletterWorkflow.CONTACT_LIST_NAME) + .emailAddress("user+ses-weekly-newsletter-2@example.com").build()))) + .thenReturn( + CreateContactResponse.builder().build()); + SendEmailResponse welcomeEmailResponse = SendEmailResponse.builder().messageId("message-id").build(); when(sesClient.sendEmail(any(SendEmailRequest.class))).thenReturn( welcomeEmailResponse); - System.setIn(new ByteArrayInputStream(baseEmail.getBytes())); - try { scenario.gatherSubscriberEmails(); } catch (Exception e) { @@ -242,6 +242,8 @@ public void test_gatherSubscriberEmails_error_contactAlreadyExists() { public void test_gatherSubscriberEmails_error_sendEmailFailed() { // Mock the necessary AWS SDK calls and responses String baseEmail = "user@example.com"; + when(scanner.nextLine()).thenReturn(baseEmail); + CreateContactResponse contactResponse = CreateContactResponse.builder().build(); when(sesClient.createContact(any(CreateContactRequest.class))).thenReturn( contactResponse); @@ -249,8 +251,6 @@ public void test_gatherSubscriberEmails_error_sendEmailFailed() { when(sesClient.sendEmail(any(SendEmailRequest.class))).thenThrow( SesV2Exception.class); - System.setIn(new ByteArrayInputStream(baseEmail.getBytes())); - try { scenario.gatherSubscriberEmails(); } catch (Exception e) { @@ -413,6 +413,8 @@ public void test_sendCouponNewsletter_error_sendingPaused() { public void test_cleanUp_success() { // Mock the necessary AWS SDK calls and responses scenario.test_setVerifiedEmail("test@example.com"); + when(scanner.nextLine()).thenReturn("y"); + DeleteContactListResponse deleteContactListResponse = DeleteContactListResponse.builder().build(); when(sesClient.deleteContactList(any(DeleteContactListRequest.class))).thenReturn(deleteContactListResponse); @@ -422,8 +424,6 @@ public void test_cleanUp_success() { DeleteEmailIdentityResponse deleteIdentityResponse = DeleteEmailIdentityResponse.builder().build(); when(sesClient.deleteEmailIdentity(any(DeleteEmailIdentityRequest.class))).thenReturn(deleteIdentityResponse); - System.setIn(new ByteArrayInputStream("y".getBytes())); - scenario.cleanUp(); String output = outContent.toString(); @@ -444,7 +444,7 @@ public void test_cleanUp_error_contactListNotFound() { DeleteEmailIdentityResponse deleteIdentityResponse = DeleteEmailIdentityResponse.builder().build(); when(sesClient.deleteEmailIdentity(any(DeleteEmailIdentityRequest.class))).thenReturn(deleteIdentityResponse); - System.setIn(new ByteArrayInputStream("y".getBytes())); + when(scanner.nextLine()).thenReturn("y"); try { scenario.cleanUp(); @@ -469,7 +469,7 @@ public void test_cleanUp_error_templateNotFound() { DeleteEmailIdentityResponse deleteIdentityResponse = DeleteEmailIdentityResponse.builder().build(); when(sesClient.deleteEmailIdentity(any(DeleteEmailIdentityRequest.class))).thenReturn(deleteIdentityResponse); - System.setIn(new ByteArrayInputStream("y".getBytes())); + when(scanner.nextLine()).thenReturn("y"); try { scenario.cleanUp(); @@ -494,7 +494,7 @@ public void test_cleanUp_error_identityNotFound() { when(sesClient.deleteEmailIdentity(any(DeleteEmailIdentityRequest.class))).thenThrow(NotFoundException.class); - System.setIn(new ByteArrayInputStream("y".getBytes())); + when(scanner.nextLine()).thenReturn("y"); try { scenario.cleanUp(); @@ -517,7 +517,7 @@ public void test_cleanUp_skipIdentityDeletion() { DeleteEmailTemplateResponse deleteTemplateResponse = DeleteEmailTemplateResponse.builder().build(); when(sesClient.deleteEmailTemplate(any(DeleteEmailTemplateRequest.class))).thenReturn(deleteTemplateResponse); - System.setIn(new ByteArrayInputStream("n".getBytes())); + when(scanner.nextLine()).thenReturn("n"); try { scenario.cleanUp(); From c67307981f26bb3789e4d0e1a97c332109d32cc0 Mon Sep 17 00:00:00 2001 From: David Souther Date: Wed, 3 Apr 2024 17:19:53 -0400 Subject: [PATCH 07/13] Workaround ListContacts GET body in Java --- .../com/example/sesv2/NewsletterWorkflow.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java b/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java index 3b5d6a94bc8..762ebf29502 100644 --- a/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java +++ b/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java @@ -36,6 +36,9 @@ public class NewsletterWorkflow { private String verifiedEmail = ""; private NewsletterScanner scanner; + // This is a temporary workaround until ListContacts GET body issue is fixed + private ArrayList contacts = new ArrayList<>(); + public void test_setVerifiedEmail(String verifiedEmail) { this.verifiedEmail = verifiedEmail; } @@ -207,6 +210,7 @@ public void gatherSubscriberEmails() throws IOException { .build(); sesClient.createContact(contactRequest); + contacts.add(emailAddress); System.out.println("Contact created: " + emailAddress); @@ -255,10 +259,19 @@ public void sendCouponNewsletter() { ListContactsRequest contactListRequest = ListContactsRequest.builder() .contactListName(CONTACT_LIST_NAME) .build(); - ListContactsResponse contactListResponse = sesClient.listContacts(contactListRequest); - List contactEmails = contactListResponse.contacts().stream() - .map(Contact::emailAddress) - .toList(); + + List contactEmails; + try { + ListContactsResponse contactListResponse = sesClient.listContacts(contactListRequest); + + contactEmails = contactListResponse.contacts().stream() + .map(Contact::emailAddress) + .toList(); + } catch (Exception e) { + // TODO: Remove when listContacts's GET body issue is resolved. + contactEmails = this.contacts; + } + // snippet-end:[sesv2.java2.newsletter.ListContacts] // Send an email using the "weekly-coupons" template to each contact in the list From 43e0338bf9040b6e35110fdb2c51afd0e1836ed3 Mon Sep 17 00:00:00 2001 From: David Souther Date: Wed, 3 Apr 2024 17:20:12 -0400 Subject: [PATCH 08/13] Improve Rust Error handling --- rustv1/examples/ses/src/newsletter.rs | 52 +++++++++++++-------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/rustv1/examples/ses/src/newsletter.rs b/rustv1/examples/ses/src/newsletter.rs index 44e66f5252f..c3bfad2d270 100644 --- a/rustv1/examples/ses/src/newsletter.rs +++ b/rustv1/examples/ses/src/newsletter.rs @@ -3,6 +3,12 @@ use anyhow::{anyhow, Result}; use aws_sdk_sesv2::{ + operation::{ + create_contact::CreateContactError, + create_contact_list::CreateContactListError, + create_email_identity::{CreateEmailIdentity, CreateEmailIdentityError}, + create_email_template::{CreateEmailTemplate, CreateEmailTemplateError}, + }, types::{ Body, Contact, Content, Destination, EmailContent, EmailTemplateContent, ListManagementOptions, Message, Template, @@ -58,16 +64,15 @@ impl<'a> SESWorkflow<'a> { .await { Ok(_) => writeln!(self.stdout, "Email identity created successfully.")?, - Err(e) => { - if e.as_service_error().unwrap().is_already_exists_exception() { + Err(e) => match e.into_service_error() { + CreateEmailIdentityError::AlreadyExistsException(_) => { writeln!( self.stdout, "Email identity already exists, skipping creation." )?; - } else { - return Err(anyhow!("Error creating email identity: {}", e)); } - } + e => return Err(anyhow!("Error creating email identity: {}", e)), + }, } // snippet-end:[sesv2.rust.create-email-identity] @@ -81,16 +86,15 @@ impl<'a> SESWorkflow<'a> { .await { Ok(_) => writeln!(self.stdout, "Contact list created successfully.")?, - Err(e) => { - if e.as_service_error().unwrap().is_already_exists_exception() { + Err(e) => match e.into_service_error() { + CreateContactListError::AlreadyExistsException(_) => { writeln!( self.stdout, "Contact list already exists, skipping creation." )?; - } else { - return Err(anyhow!("Error creating contact list: {}", e)); } - } + e => return Err(anyhow!("Error creating contact list: {}", e)), + }, } // snippet-end:[sesv2.rust.create-contact-list] @@ -118,16 +122,15 @@ impl<'a> SESWorkflow<'a> { .await { Ok(_) => writeln!(self.stdout, "Email template created successfully.")?, - Err(e) => { - if e.as_service_error().unwrap().is_already_exists_exception() { + Err(e) => match e.into_service_error() { + CreateEmailTemplateError::AlreadyExistsException(_) => { writeln!( self.stdout, "Email template already exists, skipping creation." )?; - } else { - return Err(anyhow!("Error creating email template: {}", e)); } - } + e => return Err(anyhow!("Error creating email template: {}", e)), + }, } // snippet-end:[sesv2.rust.create-email-template] @@ -167,17 +170,14 @@ impl<'a> SESWorkflow<'a> { .await { Ok(_) => writeln!(self.stdout, "Contact created for {}", email)?, - Err(e) => { - if e.as_service_error().unwrap().is_already_exists_exception() { - writeln!( - self.stdout, - "Contact already exists for {}, skipping creation.", - email - )?; - } else { - return Err(anyhow!("Error creating contact for {}: {}", email, e)); - } - } + Err(e) => match e.into_service_error() { + CreateContactError::AlreadyExistsException(_) => writeln!( + self.stdout, + "Contact already exists for {}, skipping creation.", + email + )?, + e => return Err(anyhow!("Error creating contact for {}: {}", email, e)), + }, } // snippet-end:[sesv2.rust.create-contact] From dcc4a7c07100459685a7b93f44cff05b6d987271 Mon Sep 17 00:00:00 2001 From: David Souther Date: Thu, 4 Apr 2024 10:15:48 -0400 Subject: [PATCH 09/13] Set workflow as a scenario --- .doc_gen/metadata/sesv2_metadata.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 4c0c18d7e19..de924eca1f5 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -28,6 +28,7 @@ sesv2_CreateContactList: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateContactList services: @@ -61,6 +62,7 @@ sesv2_CreateContact: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateContact services: @@ -126,6 +128,7 @@ sesv2_ListContacts: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.ListContacts services: @@ -168,6 +171,7 @@ sesv2_SendEmail_Simple: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.simple services: @@ -201,6 +205,7 @@ sesv2_SendEmail_Template: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.SendEmail.template services: @@ -226,6 +231,7 @@ sesv2_CreateEmailIdentity: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailIdentity Rust: @@ -259,6 +265,7 @@ sesv2_CreateEmailTemplate: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateEmailTemplate Rust: @@ -293,6 +300,7 @@ sesv2_DeleteContactList: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteContactList Rust: @@ -327,6 +335,7 @@ sesv2_DeleteEmailIdentity: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailIdentity Rust: @@ -361,6 +370,7 @@ sesv2_DeleteEmailTemplate: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.DeleteEmailTemplate Rust: @@ -378,7 +388,7 @@ sesv2_NewsletterWorkflow: title: A complete &SESv2; Newsletter workflow using an &AWS; SDK title_abbrev: Newsletter workflow synopsis: "&SESv2; newsletter workflow." - category: + category: Scenarios languages: Java: versions: @@ -403,6 +413,7 @@ sesv2_NewsletterWorkflow: excerpts: - description: snippet_tags: + - python.example_code.sesv2.SESv2Workflow.main - python.example_code.sesv2.SESv2Workflow.decl - python.example_code.sesv2.CreateContactList - python.example_code.sesv2.CreateContact From 51a05e3cb93f64884a1aa3c80ec1349257fa1593 Mon Sep 17 00:00:00 2001 From: David Souther Date: Thu, 4 Apr 2024 14:28:08 -0400 Subject: [PATCH 10/13] clean up --- python/example_code/sesv2/README.md | 45 +++++++++++++++++++++------ rustv1/examples/ses/README.md | 31 ++++++++++++++---- rustv1/examples/ses/src/newsletter.rs | 7 ++--- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/python/example_code/sesv2/README.md b/python/example_code/sesv2/README.md index 1f44afb5b67..6859d7b5ee2 100644 --- a/python/example_code/sesv2/README.md +++ b/python/example_code/sesv2/README.md @@ -38,16 +38,23 @@ python -m pip install -r requirements.txt Code excerpts that show you how to call individual service functions. -- [Create a contact in a contact list](newsletter.py#L152) (`CreateContact`) -- [Create a contact list](newsletter.py#L102) (`CreateContactList`) -- [Create an email identity](newsletter.py#L89) (`CreateEmailIdentity`) -- [Create an email template](newsletter.py#L115) (`CreateEmailTemplate`) -- [Delete a contact list](newsletter.py#L255) (`DeleteContactList`) -- [Delete an email identity](newsletter.py#L283) (`DeleteEmailIdentity`) -- [Delete an email template](newsletter.py#L268) (`DeleteEmailTemplate`) -- [List the contacts in a contact list](newsletter.py#L195) (`ListContacts`) -- [Send a simple email](newsletter.py#L161) (`SendEmail`) -- [Send a templated email](newsletter.py#L214) (`SendEmail`) +- [Create a contact in a contact list](newsletter.py#L155) (`CreateContact`) +- [Create a contact list](newsletter.py#L105) (`CreateContactList`) +- [Create an email identity](newsletter.py#L92) (`CreateEmailIdentity`) +- [Create an email template](newsletter.py#L118) (`CreateEmailTemplate`) +- [Delete a contact list](newsletter.py#L258) (`DeleteContactList`) +- [Delete an email identity](newsletter.py#L286) (`DeleteEmailIdentity`) +- [Delete an email template](newsletter.py#L271) (`DeleteEmailTemplate`) +- [List the contacts in a contact list](newsletter.py#L198) (`ListContacts`) +- [Send a simple email](newsletter.py#L164) (`SendEmail`) +- [Send a templated email](newsletter.py#L217) (`SendEmail`) + +### Scenarios + +Code examples that show you how to accomplish a specific task by calling multiple +functions within the same service. + +- [Newsletter workflow](newsletter.py) @@ -70,6 +77,24 @@ To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer +#### Newsletter workflow + +This example shows you how to Amazon SES v2 API newsletter workflow. + + + + + +Start the example by running the following at a command prompt: + +``` +python newsletter.py +``` + + + + + ### Tests ⚠ Running tests might result in charges to your AWS account. diff --git a/rustv1/examples/ses/README.md b/rustv1/examples/ses/README.md index 149f02ceb5a..52f84f0158b 100644 --- a/rustv1/examples/ses/README.md +++ b/rustv1/examples/ses/README.md @@ -35,16 +35,23 @@ Code excerpts that show you how to call individual service functions. - [Create a contact in a contact list](src/bin/create-contact.rs#L30) (`CreateContact`) - [Create a contact list](src/bin/create-contact-list.rs#L26) (`CreateContactList`) -- [Create an email identity](src/newsletter.rs#L49) (`CreateEmailIdentity`) -- [Create an email template](src/newsletter.rs#L94) (`CreateEmailTemplate`) -- [Delete a contact list](src/newsletter.rs#L345) (`DeleteContactList`) -- [Delete an email identity](src/newsletter.rs#L383) (`DeleteEmailIdentity`) -- [Delete an email template](src/newsletter.rs#L358) (`DeleteEmailTemplate`) +- [Create an email identity](src/newsletter.rs#L58) (`CreateEmailIdentity`) +- [Create an email template](src/newsletter.rs#L101) (`CreateEmailTemplate`) +- [Delete a contact list](src/newsletter.rs#L348) (`DeleteContactList`) +- [Delete an email identity](src/newsletter.rs#L386) (`DeleteEmailIdentity`) +- [Delete an email template](src/newsletter.rs#L361) (`DeleteEmailTemplate`) - [Get identity information](src/bin/is-email-verified.rs#L26) (`GetEmailIdentity`) - [List the contact lists](src/bin/list-contact-lists.rs#L22) (`ListContactLists`) - [List the contacts in a contact list](src/bin/list-contacts.rs#L26) (`ListContacts`) - [Send a simple email](src/bin/send-email.rs#L39) (`SendEmail`) -- [Send a templated email](src/newsletter.rs#L261) (`SendEmail`) +- [Send a templated email](src/newsletter.rs#L264) (`SendEmail`) + +### Scenarios + +Code examples that show you how to accomplish a specific task by calling multiple +functions within the same service. + +- [Newsletter workflow](src/newsletter.rs) @@ -67,6 +74,18 @@ To run the Newsletter example, copy the files from workflows/sesv2_weekly_mailer +#### Newsletter workflow + +This example shows you how to Amazon SES v2 API newsletter workflow. + + + + + + + + + ### Tests ⚠ Running tests might result in charges to your AWS account. diff --git a/rustv1/examples/ses/src/newsletter.rs b/rustv1/examples/ses/src/newsletter.rs index c3bfad2d270..3ced8145e26 100644 --- a/rustv1/examples/ses/src/newsletter.rs +++ b/rustv1/examples/ses/src/newsletter.rs @@ -4,10 +4,9 @@ use anyhow::{anyhow, Result}; use aws_sdk_sesv2::{ operation::{ - create_contact::CreateContactError, - create_contact_list::CreateContactListError, - create_email_identity::{CreateEmailIdentity, CreateEmailIdentityError}, - create_email_template::{CreateEmailTemplate, CreateEmailTemplateError}, + create_contact::CreateContactError, create_contact_list::CreateContactListError, + create_email_identity::CreateEmailIdentityError, + create_email_template::CreateEmailTemplateError, }, types::{ Body, Contact, Content, Destination, EmailContent, EmailTemplateContent, From 02aca8d6203d2ca843f4bc3d3c87fae2df05d053 Mon Sep 17 00:00:00 2001 From: David Souther Date: Thu, 4 Apr 2024 15:44:36 -0400 Subject: [PATCH 11/13] rust readme --- .../photo_asset_management/src/handlers/labels.rs | 13 ------------- rustv1/examples/ses/README.md | 12 ++++++------ 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/rustv1/cross_service/photo_asset_management/src/handlers/labels.rs b/rustv1/cross_service/photo_asset_management/src/handlers/labels.rs index 6bd982b841e..91a76ac1430 100644 --- a/rustv1/cross_service/photo_asset_management/src/handlers/labels.rs +++ b/rustv1/cross_service/photo_asset_management/src/handlers/labels.rs @@ -129,17 +129,4 @@ mod test { assert_eq!(labels.get("Mountain").expect("has Mountain").count, 3); assert_eq!(labels.get("Lake").expect("has Lake").count, 2); } - - #[test] - fn test_labels_response() { - let mut labels = Labels::new(); - labels.add("Mountain".to_string(), 3); - labels.add("River".to_string(), 5); - labels.add("Lake".to_string(), 2); - let labels_json = json!(labels); - assert_eq!( - labels_json.to_string(), - r#"{"labels":{"Lake":{"count":2},"Mountain":{"count":3},"River":{"count":5}}}"# - ) - } } diff --git a/rustv1/examples/ses/README.md b/rustv1/examples/ses/README.md index 52f84f0158b..12f31aaddd2 100644 --- a/rustv1/examples/ses/README.md +++ b/rustv1/examples/ses/README.md @@ -35,16 +35,16 @@ Code excerpts that show you how to call individual service functions. - [Create a contact in a contact list](src/bin/create-contact.rs#L30) (`CreateContact`) - [Create a contact list](src/bin/create-contact-list.rs#L26) (`CreateContactList`) -- [Create an email identity](src/newsletter.rs#L58) (`CreateEmailIdentity`) -- [Create an email template](src/newsletter.rs#L101) (`CreateEmailTemplate`) -- [Delete a contact list](src/newsletter.rs#L348) (`DeleteContactList`) -- [Delete an email identity](src/newsletter.rs#L386) (`DeleteEmailIdentity`) -- [Delete an email template](src/newsletter.rs#L361) (`DeleteEmailTemplate`) +- [Create an email identity](src/newsletter.rs#L57) (`CreateEmailIdentity`) +- [Create an email template](src/newsletter.rs#L100) (`CreateEmailTemplate`) +- [Delete a contact list](src/newsletter.rs#L347) (`DeleteContactList`) +- [Delete an email identity](src/newsletter.rs#L385) (`DeleteEmailIdentity`) +- [Delete an email template](src/newsletter.rs#L360) (`DeleteEmailTemplate`) - [Get identity information](src/bin/is-email-verified.rs#L26) (`GetEmailIdentity`) - [List the contact lists](src/bin/list-contact-lists.rs#L22) (`ListContactLists`) - [List the contacts in a contact list](src/bin/list-contacts.rs#L26) (`ListContacts`) - [Send a simple email](src/bin/send-email.rs#L39) (`SendEmail`) -- [Send a templated email](src/newsletter.rs#L264) (`SendEmail`) +- [Send a templated email](src/newsletter.rs#L263) (`SendEmail`) ### Scenarios From 88764b08c6f0a1c540da11b0803f9c4115ebb65d Mon Sep 17 00:00:00 2001 From: David Souther Date: Thu, 4 Apr 2024 22:18:09 -0400 Subject: [PATCH 12/13] rust test --- rustv1/examples/lambda/src/actions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustv1/examples/lambda/src/actions.rs b/rustv1/examples/lambda/src/actions.rs index 57079335aa2..2d14b008a13 100644 --- a/rustv1/examples/lambda/src/actions.rs +++ b/rustv1/examples/lambda/src/actions.rs @@ -567,7 +567,7 @@ mod test { assert_eq!(json!(InvokeArgs::Increment(5)), 5); assert_eq!( json!(InvokeArgs::Arithmetic(Operation::Plus, 5, 7)).to_string(), - r#"{"i":5,"j":7,"op":"plus"}"#.to_string(), + r#"{"op":"plus","i":5,"j":7}"#.to_string(), ); } } From 9f6fb7628a4fae5c9a5ce8a41b881999b980d7ac Mon Sep 17 00:00:00 2001 From: David Souther Date: Thu, 4 Apr 2024 22:26:19 -0400 Subject: [PATCH 13/13] Blacklist rustv1 "ses" readme --- .tools/readmes/config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.tools/readmes/config.py b/.tools/readmes/config.py index d34e726a5ab..7fb881c13e5 100644 --- a/.tools/readmes/config.py +++ b/.tools/readmes/config.py @@ -140,7 +140,10 @@ "base_folder": "rustv1", "service_folder": 'rustv1/examples/{{service["name"]}}', "sdk_api_ref": 'https://docs.rs/aws-sdk-{{service["name"]}}/latest/aws_sdk_{{service["name"]}}/', - "service_folder_overrides": {"sesv2": "rustv1/examples/ses"}, + "service_folder_overrides": { + "sesv2": "rustv1/examples/ses", + "ses": "rustv1/examples/_NONE_", + }, } }, "SAP ABAP": {