Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inbound email improvements (continued) #5263

Merged
merged 1 commit into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member Author

@vadimkerr vadimkerr Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call to super().validate_request is unnecessary here because django-anymail collects these validate_request methods from the whole superclass chain and runs them all in run_validators: https://github.com/anymail/django-anymail/blob/35383c7140289e82b39ada5980077898aa07d18d/anymail/webhooks/base.py#L28

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
Loading