From 5580b519e0a9af926a9e7fdc61bfe680f907f7a3 Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Wed, 9 Feb 2022 23:49:54 +0530 Subject: [PATCH] SES: Implement GetIdentityMailFromDomainAttributes and SetIdentityMailFromDomain (#4842) --- IMPLEMENTATION_COVERAGE.md | 6 +- moto/ses/models.py | 45 ++++++++++++++ moto/ses/responses.py | 52 ++++++++++++++++ tests/test_ses/test_ses_boto3.py | 101 +++++++++++++++++++++++++++++++ 4 files changed, 201 insertions(+), 3 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 76c420447..0860edad5 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -4822,7 +4822,7 @@ - [ ] get_account_sending_enabled - [ ] get_custom_verification_email_template - [ ] get_identity_dkim_attributes -- [ ] get_identity_mail_from_domain_attributes +- [X] get_identity_mail_from_domain_attributes - [X] get_identity_notification_attributes - [ ] get_identity_policies - [ ] get_identity_verification_attributes @@ -4850,7 +4850,7 @@ - [ ] set_identity_dkim_enabled - [X] set_identity_feedback_forwarding_enabled - [ ] set_identity_headers_in_notifications_enabled -- [ ] set_identity_mail_from_domain +- [X] set_identity_mail_from_domain - [X] set_identity_notification_topic - [ ] set_receipt_rule_position - [ ] test_render_template @@ -5555,4 +5555,4 @@ - workspaces - workspaces-web - xray - \ No newline at end of file + diff --git a/moto/ses/models.py b/moto/ses/models.py index 7a8206635..0cc6aab47 100644 --- a/moto/ses/models.py +++ b/moto/ses/models.py @@ -117,6 +117,7 @@ class SESBackend(BaseBackend): self.config_set = {} self.config_set_event_destination = {} self.event_destinations = {} + self.identity_mail_from_domains = {} self.templates = {} self.receipt_rule_set = {} @@ -477,5 +478,49 @@ class SESBackend(BaseBackend): else: raise RuleDoesNotExist(f"Rule does not exist: {rule['name']}") + def set_identity_mail_from_domain( + self, identity, mail_from_domain=None, behavior_on_mx_failure=None + ): + if identity not in (self.domains + self.addresses): + raise InvalidParameterValue( + "Identity '{0}' does not exist.".format(identity) + ) + + if mail_from_domain is None: + self.identity_mail_from_domains.pop(identity) + return + + if not mail_from_domain.endswith(identity): + raise InvalidParameterValue( + "Provided MAIL-FROM domain '{0}' is not subdomain of " + "the domain of the identity '{1}'.".format(mail_from_domain, identity) + ) + + if behavior_on_mx_failure not in (None, "RejectMessage", "UseDefaultValue"): + raise ValidationError( + "1 validation error detected: " + "Value '{0}' at 'behaviorOnMXFailure'" + "failed to satisfy constraint: Member must satisfy enum value set: " + "[RejectMessage, UseDefaultValue]".format(behavior_on_mx_failure) + ) + + self.identity_mail_from_domains[identity] = { + "mail_from_domain": mail_from_domain, + "behavior_on_mx_failure": behavior_on_mx_failure, + } + + def get_identity_mail_from_domain_attributes(self, identities=None): + if identities is None: + identities = [] + + attributes_by_identity = {} + for identity in identities: + if identity in (self.domains + self.addresses): + attributes_by_identity[identity] = self.identity_mail_from_domains.get( + identity + ) or {"behavior_on_mx_failure": "UseDefaultValue"} + + return attributes_by_identity + ses_backend = SESBackend() diff --git a/moto/ses/responses.py b/moto/ses/responses.py index a8409fb6b..49edac876 100644 --- a/moto/ses/responses.py +++ b/moto/ses/responses.py @@ -279,6 +279,25 @@ class EmailResponse(BaseResponse): template = self.response_template(UPDATE_RECEIPT_RULE) return template.render() + def set_identity_mail_from_domain(self): + identity = self._get_param("Identity") + mail_from_domain = self._get_param("MailFromDomain") + behavior_on_mx_failure = self._get_param("BehaviorOnMXFailure") + + ses_backend.set_identity_mail_from_domain( + identity, mail_from_domain, behavior_on_mx_failure + ) + + template = self.response_template(SET_IDENTITY_MAIL_FROM_DOMAIN) + return template.render() + + def get_identity_mail_from_domain_attributes(self): + identities = self._get_multi_param("Identities.member.") + identities = ses_backend.get_identity_mail_from_domain_attributes(identities) + template = self.response_template(GET_IDENTITY_MAIL_FROM_DOMAIN_ATTRIBUTES) + + return template.render(identities=identities) + VERIFY_EMAIL_IDENTITY = """ @@ -633,3 +652,36 @@ UPDATE_RECEIPT_RULE = """ + + + 47e0ef1a-9bf2-11e1-9279-0100e8cf109a + +""" + +GET_IDENTITY_MAIL_FROM_DOMAIN_ATTRIBUTES = """ + + {% if identities.items()|length > 0 %} + + {% for name, value in identities.items() %} + + {{ name }} + + {% if 'mail_from_domain' in value %} + {{ value.get("mail_from_domain") }} + Success + {% endif %} + {{ value.get("behavior_on_mx_failure") }} + + + {% endfor %} + + {% else %} + + {% endif %} + + + 47e0ef1a-9bf2-11e1-9279-0100e8cf109a + +""" diff --git a/tests/test_ses/test_ses_boto3.py b/tests/test_ses/test_ses_boto3.py index 909383feb..6688bc773 100644 --- a/tests/test_ses/test_ses_boto3.py +++ b/tests/test_ses/test_ses_boto3.py @@ -1230,3 +1230,104 @@ def test_get_send_statistics(): stats[0]["Rejects"].should.equal(1) stats[0]["DeliveryAttempts"].should.equal(1) + + +@mock_ses +def test_set_identity_mail_from_domain(): + conn = boto3.client("ses", region_name="eu-central-1") + + # Must raise if provided identity does not exist + with pytest.raises(ClientError) as exc: + conn.set_identity_mail_from_domain(Identity="foo.com") + err = exc.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.equal("Identity 'foo.com' does not exist.") + + conn.verify_domain_identity(Domain="foo.com") + + # Must raise if MAILFROM is not a subdomain of identity + with pytest.raises(ClientError) as exc: + conn.set_identity_mail_from_domain( + Identity="foo.com", MailFromDomain="lorem.ipsum.com" + ) + err = exc.value.response["Error"] + err["Code"].should.equal("InvalidParameterValue") + err["Message"].should.equal( + "Provided MAIL-FROM domain 'lorem.ipsum.com' is not subdomain of " + "the domain of the identity 'foo.com'." + ) + + # Must raise if BehaviorOnMXFailure is not a valid choice + with pytest.raises(ClientError) as exc: + conn.set_identity_mail_from_domain( + Identity="foo.com", + MailFromDomain="lorem.foo.com", + BehaviorOnMXFailure="SelfDestruct", + ) + err = exc.value.response["Error"] + err["Code"].should.equal("ValidationError") + err["Message"].should.equal( + "1 validation error detected: Value 'SelfDestruct' at " + "'behaviorOnMXFailure'failed to satisfy constraint: Member must " + "satisfy enum value set: [RejectMessage, UseDefaultValue]" + ) + + # Must set config for valid input + behaviour_on_mx_failure = "RejectMessage" + mail_from_domain = "lorem.foo.com" + + conn.set_identity_mail_from_domain( + Identity="foo.com", + MailFromDomain=mail_from_domain, + BehaviorOnMXFailure=behaviour_on_mx_failure, + ) + + attributes = conn.get_identity_mail_from_domain_attributes(Identities=["foo.com"]) + actual_attributes = attributes["MailFromDomainAttributes"]["foo.com"] + actual_attributes.should.have.key("MailFromDomain").being.equal(mail_from_domain) + actual_attributes.should.have.key("BehaviorOnMXFailure").being.equal( + behaviour_on_mx_failure + ) + actual_attributes.should.have.key("MailFromDomainStatus").being.equal("Success") + + # Must unset config when MailFromDomain is null + conn.set_identity_mail_from_domain(Identity="foo.com") + + attributes = conn.get_identity_mail_from_domain_attributes(Identities=["foo.com"]) + actual_attributes = attributes["MailFromDomainAttributes"]["foo.com"] + actual_attributes.should.have.key("BehaviorOnMXFailure").being.equal( + "UseDefaultValue" + ) + actual_attributes.should_not.have.key("MailFromDomain") + actual_attributes.should_not.have.key("MailFromDomainStatus") + + +@mock_ses +def test_get_identity_mail_from_domain_attributes(): + conn = boto3.client("ses", region_name="eu-central-1") + + # Must return empty for non-existent identities + attributes = conn.get_identity_mail_from_domain_attributes( + Identities=["bar@foo.com", "lorem.com"] + ) + attributes["MailFromDomainAttributes"].should.have.length_of(0) + + # Must return default options for non-configured identities + conn.verify_email_identity(EmailAddress="bar@foo.com") + attributes = conn.get_identity_mail_from_domain_attributes( + Identities=["bar@foo.com", "lorem.com"] + ) + attributes["MailFromDomainAttributes"].should.have.length_of(1) + attributes["MailFromDomainAttributes"]["bar@foo.com"].should.have.length_of(1) + attributes["MailFromDomainAttributes"]["bar@foo.com"].should.have.key( + "BehaviorOnMXFailure" + ).being.equal("UseDefaultValue") + + # Must return multiple configured identities + conn.verify_domain_identity(Domain="lorem.com") + attributes = conn.get_identity_mail_from_domain_attributes( + Identities=["bar@foo.com", "lorem.com"] + ) + attributes["MailFromDomainAttributes"].should.have.length_of(2) + attributes["MailFromDomainAttributes"]["bar@foo.com"].should.have.length_of(1) + attributes["MailFromDomainAttributes"]["lorem.com"].should.have.length_of(1)