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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user