From 1dfbeed5a72a4bd57361e44441d0d06af6a2e58a Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Wed, 14 Jun 2023 17:33:48 +0000 Subject: [PATCH] SESv2: send_email(): raw emails are send as JSON (#6405) --- moto/ses/models.py | 17 ++++++----------- moto/ses/responses.py | 2 -- moto/sesv2/responses.py | 7 ++++--- tests/test_sesv2/test_sesv2.py | 35 +++++++++++++++++++++++++++++++++- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/moto/ses/models.py b/moto/ses/models.py index f91328fa9..ffc8bf6e5 100644 --- a/moto/ses/models.py +++ b/moto/ses/models.py @@ -32,7 +32,6 @@ RECIPIENT_LIMIT = 50 class SESFeedback(BaseModel): - BOUNCE = "Bounce" COMPLAINT = "Complaint" DELIVERY = "Delivery" @@ -340,23 +339,20 @@ class SESBackend(BaseBackend): f"Did not have authority to send from email {source_email_address}" ) - recipient_count = len(destinations) message = email.message_from_string(raw_data) if source is None: if message["from"] is None: raise MessageRejectedError("Source not specified") - _, source_email_address = parseaddr(message["from"]) - if not self._is_verified_address(source_email_address): + _, source = parseaddr(message["from"]) + if not self._is_verified_address(source): raise MessageRejectedError( - f"Did not have authority to send from email {source_email_address}" + f"Did not have authority to send from email {source}" ) for header in "TO", "CC", "BCC": - recipient_count += sum( - d.strip() and 1 or 0 for d in message.get(header, "").split(",") - ) - if recipient_count > RECIPIENT_LIMIT: + destinations += [d.strip() for d in message.get(header, "").split(",") if d] + if len(destinations) > RECIPIENT_LIMIT: raise MessageRejectedError("Too many recipients.") for address in [addr for addr in [source, *destinations] if addr is not None]: msg = is_valid_address(address) @@ -365,7 +361,7 @@ class SESBackend(BaseBackend): self.__process_sns_feedback__(source, destinations) - self.sent_message_count += recipient_count + self.sent_message_count += len(destinations) message_id = get_random_message_id() raw_message = RawMessage(message_id, source, destinations, raw_data) self.sent_messages.append(raw_message) @@ -416,7 +412,6 @@ class SESBackend(BaseBackend): def create_configuration_set_event_destination( self, configuration_set_name: str, event_destination: Dict[str, Any] ) -> None: - if self.config_set.get(configuration_set_name) is None: raise ConfigurationSetDoesNotExist("Invalid Configuration Set Name.") diff --git a/moto/ses/responses.py b/moto/ses/responses.py index 6e1d9694a..03fe9a2b0 100644 --- a/moto/ses/responses.py +++ b/moto/ses/responses.py @@ -181,7 +181,6 @@ class EmailResponse(BaseResponse): return template.render() def set_identity_notification_topic(self) -> str: - identity = self.querystring.get("Identity")[0] # type: ignore not_type = self.querystring.get("NotificationType")[0] # type: ignore sns_topic = self.querystring.get("SnsTopic") # type: ignore @@ -212,7 +211,6 @@ class EmailResponse(BaseResponse): return template.render(name=configuration_set_name) def create_configuration_set_event_destination(self) -> str: - configuration_set_name = self._get_param("ConfigurationSetName") # type: ignore is_configuration_event_enabled = self.querystring.get( "EventDestination.Enabled" diff --git a/moto/sesv2/responses.py b/moto/sesv2/responses.py index e0bcbcfea..3eca20ba1 100644 --- a/moto/sesv2/responses.py +++ b/moto/sesv2/responses.py @@ -1,4 +1,5 @@ """Handles incoming sesv2 requests, invokes methods, returns responses.""" +import base64 import json from moto.core.responses import BaseResponse @@ -23,9 +24,9 @@ class SESV2Response(BaseResponse): def send_email(self) -> str: """Piggy back on functionality from v1 mostly""" - params = get_params_dict(self.querystring) + params = json.loads(self.body) from_email_address = params.get("FromEmailAddress") - destination = params.get("Destination") + destination = params.get("Destination", {}) content = params.get("Content") if "Raw" in content: all_destinations: List[str] = [] @@ -38,7 +39,7 @@ class SESV2Response(BaseResponse): message = self.sesv2_backend.send_raw_email( source=from_email_address, destinations=all_destinations, - raw_data=content["Raw"]["Data"], + raw_data=base64.b64decode(content["Raw"]["Data"]).decode("utf-8"), ) elif "Simple" in content: message = self.sesv2_backend.send_email( # type: ignore diff --git a/tests/test_sesv2/test_sesv2.py b/tests/test_sesv2/test_sesv2.py index cf608af47..2cab6638f 100644 --- a/tests/test_sesv2/test_sesv2.py +++ b/tests/test_sesv2/test_sesv2.py @@ -1,7 +1,9 @@ import boto3 from botocore.exceptions import ClientError import pytest -from moto import mock_sesv2, mock_ses +from moto import mock_sesv2, mock_ses, settings +from moto.ses.models import ses_backends, RawMessage +from tests import DEFAULT_ACCOUNT_ID from ..test_ses.test_ses_boto3 import get_raw_email @@ -63,11 +65,42 @@ def test_send_raw_email(ses_v1): # pylint: disable=redefined-outer-name ses_v1.verify_email_identity(EmailAddress="test@example.com") conn.send_email(**kwargs) + # Verify + send_quota = ses_v1.get_send_quota() + # 2 destinations in the message, two in the 'Destination'-argument + assert int(send_quota["SentLast24Hours"]) == 4 + + +@mock_sesv2 +def test_send_raw_email__with_specific_message( + ses_v1, +): # pylint: disable=redefined-outer-name + # Setup + conn = boto3.client("sesv2", region_name="us-east-1") + message = get_raw_email() + # This particular message means that our base64-encoded body contains a '=' + # Testing this to ensure that we parse the body as JSON, not as a query-dict + message["Subject"] = "Test-2" + kwargs = dict( + Content={"Raw": {"Data": message.as_bytes()}}, + ) + + # Execute + ses_v1.verify_email_identity(EmailAddress="test@example.com") + conn.send_email(**kwargs) + # Verify send_quota = ses_v1.get_send_quota() sent_count = int(send_quota["SentLast24Hours"]) assert sent_count == 2 + if not settings.TEST_SERVER_MODE: + backend = ses_backends[DEFAULT_ACCOUNT_ID]["us-east-1"] + msg: RawMessage = backend.sent_messages[0] + assert message.as_bytes() == msg.raw_data.encode("utf-8") + assert msg.source == "test@example.com" + assert msg.destinations == ["to@example.com", "foo@example.com"] + @mock_sesv2 def test_create_contact_list():