SESv2: send_email(): raw emails are send as JSON (#6405)
This commit is contained in:
parent
759f26c6d0
commit
1dfbeed5a7
@ -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.")
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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():
|
||||||
|
Loading…
Reference in New Issue
Block a user