SESv2: send_email(): raw emails are send as JSON (#6405)

This commit is contained in:
Bert Blommers 2023-06-14 17:33:48 +00:00 committed by GitHub
parent 759f26c6d0
commit 1dfbeed5a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 44 additions and 17 deletions

View File

@ -32,7 +32,6 @@ RECIPIENT_LIMIT = 50
class SESFeedback(BaseModel): class SESFeedback(BaseModel):
BOUNCE = "Bounce" BOUNCE = "Bounce"
COMPLAINT = "Complaint" COMPLAINT = "Complaint"
DELIVERY = "Delivery" DELIVERY = "Delivery"
@ -340,23 +339,20 @@ class SESBackend(BaseBackend):
f"Did not have authority to send from email {source_email_address}" f"Did not have authority to send from email {source_email_address}"
) )
recipient_count = len(destinations)
message = email.message_from_string(raw_data) message = email.message_from_string(raw_data)
if source is None: if source is None:
if message["from"] is None: if message["from"] is None:
raise MessageRejectedError("Source not specified") raise MessageRejectedError("Source not specified")
_, source_email_address = parseaddr(message["from"]) _, source = parseaddr(message["from"])
if not self._is_verified_address(source_email_address): if not self._is_verified_address(source):
raise MessageRejectedError( 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": for header in "TO", "CC", "BCC":
recipient_count += sum( destinations += [d.strip() for d in message.get(header, "").split(",") if d]
d.strip() and 1 or 0 for d in message.get(header, "").split(",") if len(destinations) > RECIPIENT_LIMIT:
)
if recipient_count > RECIPIENT_LIMIT:
raise MessageRejectedError("Too many recipients.") raise MessageRejectedError("Too many recipients.")
for address in [addr for addr in [source, *destinations] if addr is not None]: for address in [addr for addr in [source, *destinations] if addr is not None]:
msg = is_valid_address(address) msg = is_valid_address(address)
@ -365,7 +361,7 @@ class SESBackend(BaseBackend):
self.__process_sns_feedback__(source, destinations) self.__process_sns_feedback__(source, destinations)
self.sent_message_count += recipient_count self.sent_message_count += len(destinations)
message_id = get_random_message_id() message_id = get_random_message_id()
raw_message = RawMessage(message_id, source, destinations, raw_data) raw_message = RawMessage(message_id, source, destinations, raw_data)
self.sent_messages.append(raw_message) self.sent_messages.append(raw_message)
@ -416,7 +412,6 @@ class SESBackend(BaseBackend):
def create_configuration_set_event_destination( def create_configuration_set_event_destination(
self, configuration_set_name: str, event_destination: Dict[str, Any] self, configuration_set_name: str, event_destination: Dict[str, Any]
) -> None: ) -> None:
if self.config_set.get(configuration_set_name) is None: if self.config_set.get(configuration_set_name) is None:
raise ConfigurationSetDoesNotExist("Invalid Configuration Set Name.") raise ConfigurationSetDoesNotExist("Invalid Configuration Set Name.")

View File

@ -181,7 +181,6 @@ class EmailResponse(BaseResponse):
return template.render() return template.render()
def set_identity_notification_topic(self) -> str: def set_identity_notification_topic(self) -> str:
identity = self.querystring.get("Identity")[0] # type: ignore identity = self.querystring.get("Identity")[0] # type: ignore
not_type = self.querystring.get("NotificationType")[0] # type: ignore not_type = self.querystring.get("NotificationType")[0] # type: ignore
sns_topic = self.querystring.get("SnsTopic") # type: ignore sns_topic = self.querystring.get("SnsTopic") # type: ignore
@ -212,7 +211,6 @@ class EmailResponse(BaseResponse):
return template.render(name=configuration_set_name) return template.render(name=configuration_set_name)
def create_configuration_set_event_destination(self) -> str: def create_configuration_set_event_destination(self) -> str:
configuration_set_name = self._get_param("ConfigurationSetName") # type: ignore configuration_set_name = self._get_param("ConfigurationSetName") # type: ignore
is_configuration_event_enabled = self.querystring.get( is_configuration_event_enabled = self.querystring.get(
"EventDestination.Enabled" "EventDestination.Enabled"

View File

@ -1,4 +1,5 @@
"""Handles incoming sesv2 requests, invokes methods, returns responses.""" """Handles incoming sesv2 requests, invokes methods, returns responses."""
import base64
import json import json
from moto.core.responses import BaseResponse from moto.core.responses import BaseResponse
@ -23,9 +24,9 @@ class SESV2Response(BaseResponse):
def send_email(self) -> str: def send_email(self) -> str:
"""Piggy back on functionality from v1 mostly""" """Piggy back on functionality from v1 mostly"""
params = get_params_dict(self.querystring) params = json.loads(self.body)
from_email_address = params.get("FromEmailAddress") from_email_address = params.get("FromEmailAddress")
destination = params.get("Destination") destination = params.get("Destination", {})
content = params.get("Content") content = params.get("Content")
if "Raw" in content: if "Raw" in content:
all_destinations: List[str] = [] all_destinations: List[str] = []
@ -38,7 +39,7 @@ class SESV2Response(BaseResponse):
message = self.sesv2_backend.send_raw_email( message = self.sesv2_backend.send_raw_email(
source=from_email_address, source=from_email_address,
destinations=all_destinations, destinations=all_destinations,
raw_data=content["Raw"]["Data"], raw_data=base64.b64decode(content["Raw"]["Data"]).decode("utf-8"),
) )
elif "Simple" in content: elif "Simple" in content:
message = self.sesv2_backend.send_email( # type: ignore message = self.sesv2_backend.send_email( # type: ignore

View File

@ -1,7 +1,9 @@
import boto3 import boto3
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
import pytest 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 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") ses_v1.verify_email_identity(EmailAddress="test@example.com")
conn.send_email(**kwargs) 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 # Verify
send_quota = ses_v1.get_send_quota() send_quota = ses_v1.get_send_quota()
sent_count = int(send_quota["SentLast24Hours"]) sent_count = int(send_quota["SentLast24Hours"])
assert sent_count == 2 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 @mock_sesv2
def test_create_contact_list(): def test_create_contact_list():