diff --git a/.doc_gen/metadata/sesv2_metadata.yaml b/.doc_gen/metadata/sesv2_metadata.yaml index 48b1df4814d..de924eca1f5 100644 --- a/.doc_gen/metadata/sesv2_metadata.yaml +++ b/.doc_gen/metadata/sesv2_metadata.yaml @@ -13,6 +13,24 @@ 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.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl + - python.example_code.sesv2.CreateContactList services: sesv2: {CreateContactList} sesv2_CreateContact: @@ -29,6 +47,24 @@ 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.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl + - python.example_code.sesv2.CreateContact services: sesv2: {CreateContact} sesv2_GetEmailIdentity: @@ -77,12 +113,30 @@ 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.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl + - 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 +164,294 @@ 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.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl + - 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.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl + - 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.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl + - 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.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl + - 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.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl + - 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.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl + - 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.SESv2Workflow.main + - python.example_code.sesv2.SESv2Workflow.decl + - 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} + +sesv2_NewsletterWorkflow: + title: A complete &SESv2; Newsletter workflow using an &AWS; SDK + title_abbrev: Newsletter workflow + synopsis: "&SESv2; newsletter workflow." + category: Scenarios + 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.main + - 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/.tools/readmes/config.py b/.tools/readmes/config.py index 04b760f21d6..7fb881c13e5 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,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", + "ses": "rustv1/examples/_NONE_", + }, } }, "SAP ABAP": { diff --git a/javav2/example_code/ses/Readme.md b/javav2/example_code/ses/Readme.md index 000c37ad718..15786a59f73 100644 --- a/javav2/example_code/ses/Readme.md +++ b/javav2/example_code/ses/Readme.md @@ -48,6 +48,13 @@ 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/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..762ebf29502 --- /dev/null +++ b/javav2/example_code/ses/src/main/java/com/example/sesv2/NewsletterWorkflow.java @@ -0,0 +1,418 @@ +// 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.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 { + 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! + + 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 = ""; + 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; + } + + /** + * Constructor for the Workflow class. + * + * @param sesClient The SesV2Client instance to be used for interacting with the + * SES v2 service. + */ + public NewsletterWorkflow(SesV2Client sesClient, NewsletterScanner scanner) { + this.sesClient = sesClient; + this.scanner = scanner; + } + + /** + * 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().build(); + new NewsletterWorkflow(sesClient, new NewsletterScanner()).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: "); + verifiedEmail = scanner.nextLine(); + + // 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 = loadFile("resources/coupon_newsletter/coupon-newsletter.html"); + String newsletterText = loadFile("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] + } + + private String loadFile(String path) { + try { + return Files.readString(Paths.get(path)); + } catch (IOException ioe) { + return "Missing " + path; + } + } + + /** + * 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 { + System.out.print("Enter a base email address for subscribing to the newsletter: "); + String baseEmail = scanner.nextLine(); + + 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); + contacts.add(emailAddress); + + 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(); + + 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 + // 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.nextLine(); + } + + /** + * 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)"); + String input = scanner.nextLine(); + + 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] + } +} + +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 new file mode 100644 index 00000000000..c5d09aaab3b --- /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.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +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; + + @Mock + private NewsletterScanner scanner; + + @Before + public void openMocks() { + closeable = MockitoAnnotations.openMocks(this); + scenario = new NewsletterWorkflow(sesClient, scanner); + 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 + when(scanner.nextLine()).thenReturn("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); + + 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 + 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); + + 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 + when(scanner.nextLine()).thenReturn("test@example.com"); + + when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenThrow(NotFoundException.class); + + 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 + when(scanner.nextLine()).thenReturn("test@example.com"); + + when(sesClient.createEmailIdentity(any(CreateEmailIdentityRequest.class))).thenThrow(LimitExceededException.class); + + 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 + 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); + + 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 + when(scanner.nextLine()).thenReturn("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); + + 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 + when(scanner.nextLine()).thenReturn("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(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 + 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); + + scenario.gatherSubscriberEmails(); + + String output = outContent.toString(); + for (int i = 1; i <= 3; i++) { + String expectedEmail = "user+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 + 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); + + 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"; + when(scanner.nextLine()).thenReturn(baseEmail); + + CreateContactResponse contactResponse = CreateContactResponse.builder().build(); + when(sesClient.createContact(any(CreateContactRequest.class))).thenReturn( + contactResponse); + + when(sesClient.sendEmail(any(SendEmailRequest.class))).thenThrow( + SesV2Exception.class); + + 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"); + when(scanner.nextLine()).thenReturn("y"); + + 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); + + 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); + + when(scanner.nextLine()).thenReturn("y"); + + 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); + + when(scanner.nextLine()).thenReturn("y"); + + 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); + + when(scanner.nextLine()).thenReturn("y"); + + 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); + + when(scanner.nextLine()).thenReturn("n"); + + 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..6859d7b5ee2 --- /dev/null +++ b/python/example_code/sesv2/README.md @@ -0,0 +1,124 @@ +# 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#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) + + + + + +## Run the examples + +### Instructions + + + + +#### 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. + + + + + +#### 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. + + +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 \ No newline at end of file diff --git a/python/example_code/sesv2/newsletter.py b/python/example_code/sesv2/newsletter.py new file mode 100644 index 00000000000..30a0c166d22 --- /dev/null +++ b/python/example_code/sesv2/newsletter.py @@ -0,0 +1,323 @@ +# 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. + """ + try: + with open(file_path, "r") as file: + content = file.read() + return content + except Exception: + return f"Missing {file_path}" + + +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 + + +# snippet-start:[python.example_code.sesv2.SESv2Workflow.decl] +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 + + # snippet-end:[python.example_code.sesv2.SESv2Workflow.decl] + + 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] + 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}'.") + 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: + +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): + """ + 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.") + + +# snippet-start:[python.example_code.sesv2.SESv2Workflow.main] +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() + + +# snippet-end:[python.example_code.sesv2.SESv2Workflow.main] + + +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..10633b9a41e --- /dev/null +++ b/python/example_code/sesv2/newsletter_test.py @@ -0,0 +1,338 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json +import sys +from botocore.exceptions import ClientError +from io import StringIO +import pytest +from unittest.mock import patch + +from newsletter import ( + SESv2Workflow, + get_subaddress_variants, + CONTACT_LIST_NAME, + TEMPLATE_NAME, +) + +# Run tests with `python -m unittest` + + +class TestSESv2WorkflowPrepareApplication: + @patch("boto3.client") + def setup_method(self, method, 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" + 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): + 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" + 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): + 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 pytest.raises(ClientError): + self.workflow.prepare_application() + sys.stdout = sys.__stdout__ + + 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): + 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 pytest.raises(ClientError): + self.workflow.prepare_application() + sys.stdout = sys.__stdout__ + + 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): + 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 pytest.raises(ClientError): + self.workflow.prepare_application() + sys.stdout = sys.__stdout__ + + expected_output = ( + "Email identity 'verified@example.com' created successfully.\n" + ) + assert captured_output.getvalue() == expected_output + + +class TestSESv2WorkflowGatherSubscribers: + @patch("boto3.client") + def setup_method(self, method, mock_client): + self.ses_client = mock_client() + self.workflow = SESv2Workflow(self.ses_client) + 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" + ) + 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): + 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" + assert 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 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" + assert captured_output.getvalue() == expected_output + + +class TestSESv2WorkflowSendCouponNewsletter: + @patch("boto3.client") + def setup_method(self, method, mock_client): + self.ses_client = mock_client() + self.workflow = SESv2Workflow(self.ses_client) + self.workflow.verified_email = "verified@example.com" + + # Tests for send_coupon_newsletter + @patch( + "newsletter.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" + 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( + 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" + assert captured_output.getvalue() == expected_output + + @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 + ): + 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" + assert captured_output.getvalue() == expected_output + + +class TestSESv2WorkflowCleanUp: + @patch("boto3.client") + def setup_method(self, method, mock_client): + self.ses_client = mock_client() + self.workflow = SESv2Workflow(self.ses_client) + 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" + assert 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" + assert 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" + assert 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" + assert 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/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/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(), ); } } 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..12f31aaddd2 100644 --- a/rustv1/examples/ses/README.md +++ b/rustv1/examples/ses/README.md @@ -1,13 +1,13 @@ -# 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 @@ -29,6 +29,31 @@ For prerequisites, see the [README](../../README.md#Prerequisites) in the `rustv +### 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#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#L263) (`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) + + @@ -38,10 +63,29 @@ For prerequisites, see the [README](../../README.md#Prerequisites) in the `rustv + +#### 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. + +#### 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. @@ -57,9 +101,9 @@ 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/) diff --git a/rustv1/examples/ses/src/bin/newsletter.rs b/rustv1/examples/ses/src/bin/newsletter.rs new file mode 100644 index 00000000000..eb865d6e1da --- /dev/null +++ b/rustv1/examples/ses/src/bin/newsletter.rs @@ -0,0 +1,48 @@ +// 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; + +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..356ec39940d --- /dev/null +++ b/rustv1/examples/ses/src/lib.rs @@ -0,0 +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 new file mode 100644 index 00000000000..3ced8145e26 --- /dev/null +++ b/rustv1/examples/ses/src/newsletter.rs @@ -0,0 +1,419 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{anyhow, Result}; +use aws_sdk_sesv2::{ + operation::{ + create_contact::CreateContactError, create_contact_list::CreateContactListError, + create_email_identity::CreateEmailIdentityError, + create_email_template::CreateEmailTemplateError, + }, + 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) => match e.into_service_error() { + CreateEmailIdentityError::AlreadyExistsException(_) => { + writeln!( + self.stdout, + "Email identity already exists, skipping creation." + )?; + } + e => 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) => match e.into_service_error() { + CreateContactListError::AlreadyExistsException(_) => { + writeln!( + self.stdout, + "Contact list already exists, skipping creation." + )?; + } + e => 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 = + 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) + .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) => match e.into_service_error() { + CreateEmailTemplateError::AlreadyExistsException(_) => { + writeln!( + self.stdout, + "Email template already exists, skipping creation." + )?; + } + e => 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) => 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] + + // Send the welcome email + // snippet-start:[sesv2.rust.send-email.simple] + 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() + .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 = 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) + .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..c430269b111 --- /dev/null +++ b/rustv1/examples/ses/tests/test_newsletter.rs @@ -0,0 +1,835 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +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..1123464bff3 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) @@ -57,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) --- 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