Skip to content

Commit

Permalink
Inbound email improvements (continued)
Browse files Browse the repository at this point in the history
  • Loading branch information
vadimkerr committed Nov 18, 2024
1 parent 10dc454 commit 095e7ef
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 44 deletions.
11 changes: 7 additions & 4 deletions engine/apps/email/inbound.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from functools import cached_property
from typing import Optional, TypedDict

import requests
from anymail.exceptions import AnymailAPIError, AnymailInvalidAddress, AnymailWebhookValidationFailure
from anymail.inbound import AnymailInboundMessage
from anymail.signals import AnymailInboundEvent
Expand All @@ -26,12 +27,14 @@ class AmazonSESValidatedInboundWebhookView(amazon_ses.AmazonSESInboundWebhookVie

def validate_request(self, request):
"""Add SNS message validation to Amazon SES inbound webhook view, which is not implemented in Anymail."""

super().validate_request(request)
sns_message = self._parse_sns_message(request)
if not validate_amazon_sns_message(sns_message):
if not validate_amazon_sns_message(self._parse_sns_message(request)):
raise AnymailWebhookValidationFailure("SNS message validation failed")

def auto_confirm_sns_subscription(self, sns_message):
"""This method is called after validate_request, so we can be sure that the message is valid."""
response = requests.get(sns_message["SubscribeURL"])
response.raise_for_status()


# {<ESP name>: (<django-anymail inbound webhook view class>, <webhook secret argument name to pass to the view>), ...}
INBOUND_EMAIL_ESP_OPTIONS = {
Expand Down
148 changes: 108 additions & 40 deletions engine/apps/email/tests/test_inbound_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
)
AMAZON_SNS_TOPIC_ARN = "arn:aws:sns:us-east-2:123456789012:test"
SIGNING_CERT_URL = "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-example.pem"
SENDER_EMAIL = "[email protected]"
TO_EMAIL = "[email protected]"
SUBJECT = "Test email"
MESSAGE = "This is a test email message body."


def _sns_inbound_email_payload_and_headers(sender_email, to_email, subject, message):
Expand Down Expand Up @@ -439,15 +443,11 @@ def test_amazon_ses_pass(create_alert_mock, settings, make_organization, make_al
token="test-token",
)

sender_email = "[email protected]"
to_email = "[email protected]"
subject = "Test email"
message = "This is a test email message body."
sns_payload, sns_headers = _sns_inbound_email_payload_and_headers(
sender_email=sender_email,
to_email=to_email,
subject=subject,
message=message,
sender_email=SENDER_EMAIL,
to_email=TO_EMAIL,
subject=SUBJECT,
message=MESSAGE,
)

client = APIClient()
Expand All @@ -460,16 +460,16 @@ def test_amazon_ses_pass(create_alert_mock, settings, make_organization, make_al

assert response.status_code == status.HTTP_200_OK
create_alert_mock.assert_called_once_with(
title=subject,
message=message,
title=SUBJECT,
message=MESSAGE,
alert_receive_channel_pk=alert_receive_channel.pk,
image_url=None,
link_to_upstream_details=None,
integration_unique_data=None,
raw_request_data={
"subject": subject,
"message": message,
"sender": sender_email,
"subject": SUBJECT,
"message": MESSAGE,
"sender": SENDER_EMAIL,
},
received_at=ANY,
)
Expand All @@ -493,15 +493,11 @@ def test_amazon_ses_validated_pass(
token="test-token",
)

sender_email = "[email protected]"
to_email = "[email protected]"
subject = "Test email"
message = "This is a test email message body."
sns_payload, sns_headers = _sns_inbound_email_payload_and_headers(
sender_email=sender_email,
to_email=to_email,
subject=subject,
message=message,
sender_email=SENDER_EMAIL,
to_email=TO_EMAIL,
subject=SUBJECT,
message=MESSAGE,
)

client = APIClient()
Expand All @@ -514,23 +510,100 @@ def test_amazon_ses_validated_pass(

assert response.status_code == status.HTTP_200_OK
mock_create_alert.assert_called_once_with(
title=subject,
message=message,
title=SUBJECT,
message=MESSAGE,
alert_receive_channel_pk=alert_receive_channel.pk,
image_url=None,
link_to_upstream_details=None,
integration_unique_data=None,
raw_request_data={
"subject": subject,
"message": message,
"sender": sender_email,
"subject": SUBJECT,
"message": MESSAGE,
"sender": SENDER_EMAIL,
},
received_at=ANY,
)

mock_requests_get.assert_called_once_with(SIGNING_CERT_URL, timeout=5)


@patch("requests.get", return_value=Mock(content=CERTIFICATE))
@patch.object(create_alert, "delay")
@pytest.mark.django_db
def test_amazon_ses_validated_fail_wrong_sns_topic_arn(
mock_create_alert, mock_requests_get, settings, make_organization, make_alert_receive_channel
):
settings.INBOUND_EMAIL_ESP = "amazon_ses_validated,mailgun"
settings.INBOUND_EMAIL_DOMAIN = "inbound.example.com"
settings.INBOUND_EMAIL_WEBHOOK_SECRET = "secret"
settings.INBOUND_EMAIL_AMAZON_SNS_TOPIC_ARN = "arn:aws:sns:us-east-2:123456789013:test"

organization = make_organization()
make_alert_receive_channel(
organization,
integration=AlertReceiveChannel.INTEGRATION_INBOUND_EMAIL,
token="test-token",
)

sns_payload, sns_headers = _sns_inbound_email_payload_and_headers(
sender_email=SENDER_EMAIL,
to_email=TO_EMAIL,
subject=SUBJECT,
message=MESSAGE,
)

client = APIClient()
response = client.post(
reverse("integrations:inbound_email_webhook"),
data=sns_payload,
headers=sns_headers,
format="json",
)

assert response.status_code == status.HTTP_400_BAD_REQUEST
mock_create_alert.assert_not_called()
mock_requests_get.assert_not_called()


@patch("requests.get", return_value=Mock(content=CERTIFICATE))
@patch.object(create_alert, "delay")
@pytest.mark.django_db
def test_amazon_ses_validated_fail_wrong_signature(
mock_create_alert, mock_requests_get, settings, make_organization, make_alert_receive_channel
):
settings.INBOUND_EMAIL_ESP = "amazon_ses_validated,mailgun"
settings.INBOUND_EMAIL_DOMAIN = "inbound.example.com"
settings.INBOUND_EMAIL_WEBHOOK_SECRET = "secret"
settings.INBOUND_EMAIL_AMAZON_SNS_TOPIC_ARN = AMAZON_SNS_TOPIC_ARN

organization = make_organization()
make_alert_receive_channel(
organization,
integration=AlertReceiveChannel.INTEGRATION_INBOUND_EMAIL,
token="test-token",
)

sns_payload, sns_headers = _sns_inbound_email_payload_and_headers(
sender_email=SENDER_EMAIL,
to_email=TO_EMAIL,
subject=SUBJECT,
message=MESSAGE,
)
sns_payload["Signature"] = "invalid-signature"

client = APIClient()
response = client.post(
reverse("integrations:inbound_email_webhook"),
data=sns_payload,
headers=sns_headers,
format="json",
)

assert response.status_code == status.HTTP_400_BAD_REQUEST
mock_create_alert.assert_not_called()
mock_requests_get.assert_called_once_with(SIGNING_CERT_URL, timeout=5)


@patch.object(create_alert, "delay")
@pytest.mark.django_db
def test_mailgun_pass(create_alert_mock, settings, make_organization, make_alert_receive_channel):
Expand All @@ -545,16 +618,11 @@ def test_mailgun_pass(create_alert_mock, settings, make_organization, make_alert
token="test-token",
)

sender_email = "[email protected]"
to_email = "[email protected]"
subject = "Test email"
message = "This is a test email message body."

mailgun_payload = _mailgun_inbound_email_payload(
sender_email=sender_email,
to_email=to_email,
subject=subject,
message=message,
sender_email=SENDER_EMAIL,
to_email=TO_EMAIL,
subject=SUBJECT,
message=MESSAGE,
)

client = APIClient()
Expand All @@ -566,16 +634,16 @@ def test_mailgun_pass(create_alert_mock, settings, make_organization, make_alert

assert response.status_code == status.HTTP_200_OK
create_alert_mock.assert_called_once_with(
title=subject,
message=message,
title=SUBJECT,
message=MESSAGE,
alert_receive_channel_pk=alert_receive_channel.pk,
image_url=None,
link_to_upstream_details=None,
integration_unique_data=None,
raw_request_data={
"subject": subject,
"message": message,
"sender": sender_email,
"subject": SUBJECT,
"message": MESSAGE,
"sender": SENDER_EMAIL,
},
received_at=ANY,
)
Expand Down

0 comments on commit 095e7ef

Please sign in to comment.