From 2d6f04d01c8cb05424d4aeaa8985e9e545330306 Mon Sep 17 00:00:00 2001 From: deepaksrgm2010 Date: Thu, 11 Aug 2022 19:13:33 +0200 Subject: [PATCH] SES: Implement SendBulkTemplatedEmail (#5361) --- moto/ses/models.py | 41 +++++++++++++ moto/ses/responses.py | 49 +++++++++++++++- tests/test_ses/test_ses_boto3.py | 98 +++++++++++++++++++++++++++++++- 3 files changed, 183 insertions(+), 5 deletions(-) diff --git a/moto/ses/models.py b/moto/ses/models.py index feaafbae3..52cbfe9ec 100644 --- a/moto/ses/models.py +++ b/moto/ses/models.py @@ -79,6 +79,15 @@ class TemplateMessage(BaseModel): self.destinations = destinations +class BulkTemplateMessage(BaseModel): + def __init__(self, message_ids, source, template, template_data, destinations): + self.ids = message_ids + self.source = source + self.template = template + self.template_data = template_data + self.destinations = destinations + + class RawMessage(BaseModel): def __init__(self, message_id, source, destinations, raw_data): self.id = message_id @@ -181,6 +190,38 @@ class SESBackend(BaseBackend): self.sent_message_count += recipient_count return message + def send_bulk_templated_email( + self, source, template, template_data, destinations, region + ): + recipient_count = len(destinations) + if recipient_count > RECIPIENT_LIMIT: + raise MessageRejectedError("Too many destinations.") + + total_recipient_count = sum( + map(lambda d: sum(map(len, d["Destination"].values())), destinations) + ) + if total_recipient_count > RECIPIENT_LIMIT: + raise MessageRejectedError("Too many destinations.") + + if not self._is_verified_address(source): + self.rejected_messages_count += 1 + raise MessageRejectedError("Email address not verified %s" % source) + + if not self.templates.get(template[0]): + raise TemplateDoesNotExist("Template (%s) does not exist" % template[0]) + + self.__process_sns_feedback__(source, destinations, region) + + message_id = get_random_message_id() + message = TemplateMessage( + message_id, source, template, template_data, destinations + ) + self.sent_messages.append(message) + self.sent_message_count += total_recipient_count + + ids = list(map(lambda x: get_random_message_id(), range(len(destinations)))) + return BulkTemplateMessage(ids, source, template, template_data, destinations) + def send_templated_email( self, source, template, template_data, destinations, region ): diff --git a/moto/ses/responses.py b/moto/ses/responses.py index 3fb5f8cc0..e2cf56742 100644 --- a/moto/ses/responses.py +++ b/moto/ses/responses.py @@ -94,6 +94,40 @@ class EmailResponse(BaseResponse): template = self.response_template(SEND_TEMPLATED_EMAIL_RESPONSE) return template.render(message=message) + def send_bulk_templated_email(self): + source = self.querystring.get("Source")[0] + template = self.querystring.get("Template") + template_data = self.querystring.get("DefaultTemplateData") + + destinations = [] + for i in range(1, 52): + destination_field = ( + "Destinations.member.%s.Destination.ToAddresses.member.1" % (i) + ) + if self.querystring.get(destination_field) is None: + break + destination = {"ToAddresses": [], "CcAddresses": [], "BccAddresses": []} + for dest_type in destination: + # consume up to 51 to allow exception + for j in range(1, 52): + field = "Destinations.member.%s.Destination.%s.member.%s" % ( + i, + dest_type, + j, + ) + address = self.querystring.get(field) + if address is None: + break + destination[dest_type].append(address[0]) + destinations.append({"Destination": destination}) + + message = self.backend.send_bulk_templated_email( + source, template, template_data, destinations, self.region + ) + template = self.response_template(SEND_BULK_TEMPLATED_EMAIL_RESPONSE) + result = template.render(message=message) + return result + def send_raw_email(self): source = self.querystring.get("Source") if source is not None: @@ -401,6 +435,19 @@ SEND_TEMPLATED_EMAIL_RESPONSE = """ + + {% for id in message.ids %} + + {{ id }} + + {% endfor %} + + + d5964849-c866-11e0-9beb-01a62d68c57f + +""" + SEND_RAW_EMAIL_RESPONSE = """ {{ message.id }} @@ -486,7 +533,6 @@ CREATE_CONFIGURATION_SET = """ @@ -522,7 +568,6 @@ GET_TEMPLATE = """ diff --git a/tests/test_ses/test_ses_boto3.py b/tests/test_ses/test_ses_boto3.py index 205699ffb..2db46c7a6 100644 --- a/tests/test_ses/test_ses_boto3.py +++ b/tests/test_ses/test_ses_boto3.py @@ -1,14 +1,11 @@ import json - import boto3 from botocore.exceptions import ClientError from botocore.exceptions import ParamValidationError from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText import pytest - import sure # noqa # pylint: disable=unused-import - from moto import mock_ses @@ -174,6 +171,101 @@ def test_send_email_invalid_address(): err["Message"].should.equal("Missing domain") +@mock_ses +def test_send_bulk_templated_email(): + conn = boto3.client("ses", region_name="us-east-1") + + kwargs = dict( + Source="test@example.com", + Destinations=[ + { + "Destination": { + "ToAddresses": ["test_to@example.com"], + "CcAddresses": ["test_cc@example.com"], + "BccAddresses": ["test_bcc@example.com"], + } + }, + { + "Destination": { + "ToAddresses": ["test_to1@example.com"], + "CcAddresses": ["test_cc1@example.com"], + "BccAddresses": ["test_bcc1@example.com"], + } + }, + ], + Template="test_template", + DefaultTemplateData='{"name": "test"}', + ) + + with pytest.raises(ClientError) as ex: + conn.send_bulk_templated_email(**kwargs) + + ex.value.response["Error"]["Code"].should.equal("MessageRejected") + ex.value.response["Error"]["Message"].should.equal( + "Email address not verified test@example.com" + ) + + conn.verify_domain_identity(Domain="example.com") + + with pytest.raises(ClientError) as ex: + conn.send_bulk_templated_email(**kwargs) + + ex.value.response["Error"]["Code"].should.equal("TemplateDoesNotExist") + + conn.create_template( + Template={ + "TemplateName": "test_template", + "SubjectPart": "lalala", + "HtmlPart": "", + "TextPart": "", + } + ) + + conn.send_bulk_templated_email(**kwargs) + + too_many_destinations = list( + { + "Destination": { + "ToAddresses": ["to%s@example.com" % i], + "CcAddresses": [], + "BccAddresses": [], + } + } + for i in range(51) + ) + + with pytest.raises(ClientError) as ex: + args = dict(kwargs, Destinations=too_many_destinations) + conn.send_bulk_templated_email(**args) + + ex.value.response["Error"]["Code"].should.equal("MessageRejected") + ex.value.response["Error"]["Message"].should.equal("Too many destinations.") + + too_many_destinations = list("to%s@example.com" % i for i in range(51)) + + with pytest.raises(ClientError) as ex: + args = dict( + kwargs, + Destinations=[ + { + "Destination": { + "ToAddresses": too_many_destinations, + "CcAddresses": [], + "BccAddresses": [], + } + } + ], + ) + conn.send_bulk_templated_email(**args) + + ex.value.response["Error"]["Code"].should.equal("MessageRejected") + ex.value.response["Error"]["Message"].should.equal("Too many destinations.") + + send_quota = conn.get_send_quota() + sent_count = int(send_quota["SentLast24Hours"]) + sent_count.should.equal(6) + + @mock_ses def test_send_templated_email(): conn = boto3.client("ses", region_name="us-east-1")