oncall-engine/engine/apps/email/tests/test_inbound_email.py
Vadim Stepanov c36761e345
Inbound email: download from S3 + convert HTML to plaintext (#5348)
# What this PR does

* Make `AmazonSESValidatedInboundWebhookView` able to download emails
from S3 by providing AWS credentials via env variables
* Convert HTML to plaintext when there's only `text/html` available

## Which issue(s) this PR closes

Related to https://github.com/grafana/oncall-private/issues/2905

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.
2024-12-18 16:35:44 +00:00

843 lines
30 KiB
Python

import datetime
import hashlib
import hmac
import json
from base64 import b64encode
from textwrap import dedent
from unittest.mock import ANY, Mock, patch
import httpretty
import pytest
from anymail.inbound import AnymailInboundMessage
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.x509 import CertificateBuilder, NameOID
from django.conf import settings
from django.urls import reverse
from requests import RequestException
from rest_framework import status
from rest_framework.test import APIClient
from apps.alerts.models import AlertReceiveChannel
from apps.email.inbound import InboundEmailWebhookView
from apps.integrations.tasks import create_alert
PRIVATE_KEY = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
ISSUER_NAME = x509.Name(
[
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Test"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "Test"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Amazon"),
x509.NameAttribute(NameOID.COMMON_NAME, "Test"),
]
)
CERTIFICATE = (
CertificateBuilder()
.subject_name(ISSUER_NAME)
.issuer_name(ISSUER_NAME)
.public_key(PRIVATE_KEY.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.now() - datetime.timedelta(days=1))
.not_valid_after(datetime.datetime.now() + datetime.timedelta(days=10))
.sign(PRIVATE_KEY, hashes.SHA256())
.public_bytes(serialization.Encoding.PEM)
)
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 = "sender@example.com"
TO_EMAIL = "test-token@inbound.example.com"
SUBJECT = "Test email"
MESSAGE = "This is a test email message body."
def _sns_inbound_email_setup(sender_email, to_email, subject, message, content_type="text/plain", s3=False):
content = (
f"From: Sender Name <{sender_email}>\n"
f"To: {to_email}\n"
f"Subject: {subject}\n"
"Date: Tue, 5 Nov 2024 16:05:39 +0000\n"
"Message-ID: <example-message-id@mail.example.com>\n"
f"Content-Type: {content_type}\n\n"
f"{message}\r\n"
)
message = {
"notificationType": "Received",
"mail": {
"timestamp": "2024-11-05T16:05:52.387Z",
"source": sender_email,
"messageId": "example-message-id-5678",
"destination": [to_email],
"headersTruncated": False,
"headers": [
{"name": "Return-Path", "value": f"<{sender_email}>"},
{
"name": "Received",
"value": (
f"from mail.example.com (mail.example.com [203.0.113.1]) "
f"by inbound-smtp.us-east-2.amazonaws.com with SMTP id example-id "
f"for {to_email}; Tue, 05 Nov 2024 16:05:52 +0000 (UTC)"
),
},
{"name": "X-SES-Spam-Verdict", "value": "PASS"},
{"name": "X-SES-Virus-Verdict", "value": "PASS"},
{
"name": "Received-SPF",
"value": (
"pass (spfCheck: domain of example.com designates 203.0.113.1 as permitted sender) "
f"client-ip=203.0.113.1; envelope-from={sender_email}; helo=mail.example.com;"
),
},
{
"name": "Authentication-Results",
"value": (
"amazonses.com; spf=pass (spfCheck: domain of example.com designates 203.0.113.1 as permitted sender) "
f"client-ip=203.0.113.1; envelope-from={sender_email}; helo=mail.example.com; "
"dkim=pass header.i=@example.com; dmarc=pass header.from=example.com;"
),
},
{"name": "X-SES-RECEIPT", "value": "example-receipt-data"},
{"name": "X-SES-DKIM-SIGNATURE", "value": "example-dkim-signature"},
{
"name": "Received",
"value": (
f"by mail.example.com with SMTP id example-id for <{to_email}>; "
"Tue, 05 Nov 2024 08:05:52 -0800 (PST)"
),
},
{
"name": "DKIM-Signature",
"value": (
"v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=default; t=1234567890; "
"bh=examplehash; h=From:To:Subject:Date:Message-ID; b=example-signature"
),
},
{"name": "X-Google-DKIM-Signature", "value": "example-google-dkim-signature"},
{"name": "X-Gm-Message-State", "value": "example-message-state"},
{"name": "X-Google-Smtp-Source", "value": "example-smtp-source"},
{
"name": "X-Received",
"value": "by 2002:a17:example with SMTP id example-id; Tue, 05 Nov 2024 08:05:50 -0800 (PST)",
},
{"name": "MIME-Version", "value": "1.0"},
{"name": "From", "value": f"Sender Name <{sender_email}>"},
{"name": "Date", "value": "Tue, 5 Nov 2024 16:05:39 +0000"},
{"name": "Message-ID", "value": "<example-message-id@mail.example.com>"},
{"name": "Subject", "value": subject},
{"name": "To", "value": to_email},
{
"name": "Content-Type",
"value": f"{content_type}",
},
],
"commonHeaders": {
"returnPath": sender_email,
"from": [f"Sender Name <{sender_email}>"],
"date": "Tue, 5 Nov 2024 16:05:39 +0000",
"to": [to_email],
"messageId": "<example-message-id@mail.example.com>",
"subject": subject,
},
},
"receipt": {
"timestamp": "2024-11-05T16:05:52.387Z",
"processingTimeMillis": 638,
"recipients": [to_email],
"spamVerdict": {"status": "PASS"},
"virusVerdict": {"status": "PASS"},
"spfVerdict": {"status": "PASS"},
"dkimVerdict": {"status": "PASS"},
"dmarcVerdict": {"status": "PASS"},
"action": {
"type": "S3" if s3 else "SNS",
"topicArn": "arn:aws:sns:us-east-2:123456789012:test",
**({"bucketName": "test-s3-bucket", "objectKey": "test-object-key"} if s3 else {"encoding": "BASE64"}),
},
},
**({} if s3 else {"content": b64encode(content.encode()).decode()}),
}
payload = {
"Type": "Notification",
"MessageId": "example-message-id-1234",
"TopicArn": AMAZON_SNS_TOPIC_ARN,
"Subject": "Amazon SES Email Receipt Notification",
"Message": json.dumps(message),
"Timestamp": "2024-11-05T16:05:53.041Z",
"SignatureVersion": "1",
"SigningCertURL": SIGNING_CERT_URL,
"UnsubscribeURL": (
"https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn="
"arn:aws:sns:us-east-2:123456789012:test:example-subscription-id"
),
}
# Sign the payload
canonical_message = "".join(
f"{key}\n{payload[key]}\n" for key in ("Message", "MessageId", "Subject", "Timestamp", "TopicArn", "Type")
)
signature = PRIVATE_KEY.sign(
canonical_message.encode(),
padding.PKCS1v15(),
hashes.SHA1(),
)
payload["Signature"] = b64encode(signature).decode()
headers = {
"X-Amz-Sns-Message-Type": "Notification",
"X-Amz-Sns-Message-Id": "example-message-id-1234",
}
return payload, headers, content
def _mailgun_inbound_email_payload(sender_email, to_email, subject, message):
timestamp, token = "1731341416", "example-token"
signature = hmac.new(
key=settings.INBOUND_EMAIL_WEBHOOK_SECRET.encode("ascii"),
msg="{}{}".format(timestamp, token).encode("ascii"),
digestmod=hashlib.sha256,
).hexdigest()
return {
"Content-Type": 'multipart/alternative; boundary="000000000000267130626a556e5"',
"Date": "Mon, 11 Nov 2024 16:10:03 +0000",
"Dkim-Signature": (
"v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=default; "
"t=1731341415; x=1731946215; darn=example.com; "
"h=to:subject:message-id:date:from:mime-version:from:to:cc:subject "
":date:message-id:reply-to; bh=examplebh; b=exampleb"
),
"From": f"Sender Name <{sender_email}>",
"Message-Id": "<example-message-id@mail.example.com>",
"Mime-Version": "1.0",
"Received": (
f"by mail.example.com with SMTP id example-id for <{to_email}>; " "Mon, 11 Nov 2024 08:10:15 -0800 (PST)"
),
"Subject": subject,
"To": to_email,
"X-Envelope-From": sender_email,
"X-Gm-Message-State": "example-message-state",
"X-Google-Dkim-Signature": (
"v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; "
"t=1731341415; x=1731946215; "
"h=to:subject:message-id:date:from:mime-version:x-gm-message-state "
":from:to:cc:subject:date:message-id:reply-to; bh=examplebh; b=exampleb"
),
"X-Google-Smtp-Source": "example-smtp-source",
"X-Mailgun-Incoming": "Yes",
"X-Received": "by 2002:a17:example with SMTP id example-id; Mon, 11 Nov 2024 08:10:14 -0800 (PST)",
"body-html": f'<div dir="ltr">{message}<br></div>\r\n',
"body-plain": f"{message}\r\n",
"from": f"Sender Name <{sender_email}>",
"message-headers": json.dumps(
[
["X-Mailgun-Incoming", "Yes"],
["X-Envelope-From", sender_email],
[
"Received",
(
"from mail.example.com (mail.example.com [203.0.113.1]) "
"by example.com with SMTP id example-id; "
"Mon, 11 Nov 2024 16:10:15 GMT"
),
],
[
"Received",
(
f"by mail.example.com with SMTP id example-id for <{to_email}>; "
"Mon, 11 Nov 2024 08:10:15 -0800 (PST)"
),
],
[
"Dkim-Signature",
(
"v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=default; "
"t=1731341415; x=1731946215; darn=example.com; "
"h=to:subject:message-id:date:from:mime-version:from:to:cc:subject "
":date:message-id:reply-to; bh=examplebh; b=exampleb"
),
],
[
"X-Google-Dkim-Signature",
(
"v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; "
"t=1731341415; x=1731946215; "
"h=to:subject:message-id:date:from:mime-version:x-gm-message-state "
":from:to:cc:subject:date:message-id:reply-to; bh=examplebh; b=exampleb"
),
],
["X-Gm-Message-State", "example-message-state"],
["X-Google-Smtp-Source", "example-smtp-source"],
[
"X-Received",
"by 2002:a17:example with SMTP id example-id; Mon, 11 Nov 2024 08:10:14 -0800 (PST)",
],
["Mime-Version", "1.0"],
["From", f"Sender Name <{sender_email}>"],
["Date", "Mon, 11 Nov 2024 16:10:03 +0000"],
["Message-Id", "<example-message-id@mail.example.com>"],
["Subject", subject],
["To", to_email],
[
"Content-Type",
'multipart/alternative; boundary="000000000000267130626a556e5"',
],
]
),
"recipient": to_email,
"sender": sender_email,
"signature": signature,
"stripped-html": f'<div dir="ltr">{message}<br></div>\n',
"stripped-text": f"{message}\n",
"subject": subject,
"timestamp": timestamp,
"token": token,
}
@pytest.mark.parametrize(
"recipients,expected",
[
("{token}@example.com", status.HTTP_200_OK),
("{token}@example.com, another@example.com", status.HTTP_200_OK),
("{token}@example.com, another@example.com", status.HTTP_200_OK),
("{token}@other.com, {token}@example.com", status.HTTP_400_BAD_REQUEST),
],
)
@pytest.mark.django_db
@pytest.mark.filterwarnings("ignore:::anymail.*") # ignore missing WEBHOOK_SECRET in amazon ses test setup
def test_amazon_ses_provider_load(
settings, make_organization_and_user_with_token, make_alert_receive_channel, recipients, expected
):
settings.INBOUND_EMAIL_ESP = "amazon_ses"
settings.INBOUND_EMAIL_DOMAIN = "example.com"
dummy_channel_token = "dummy-channel-token"
organization, _, token = make_organization_and_user_with_token()
_ = make_alert_receive_channel(organization, token=dummy_channel_token)
recipients = recipients.format(token=dummy_channel_token)
mime = f"""From: sender@example.com
Subject: Dummy email message
To: {recipients}
Content-Type: text/plain
Hello!
"""
message = {
"notificationType": "Received",
"receipt": {"action": {"type": "SNS"}, "recipients": recipients.split(",")},
"content": mime,
}
dummy_sns_message_id = "22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324"
dummy_sns_payload = {
"Type": "Notification",
"MessageId": dummy_sns_message_id,
"TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic",
"Subject": "My First Message",
"Message": json.dumps(message),
}
client = APIClient()
response = client.post(
reverse("integrations:inbound_email_webhook"),
data=json.dumps(dummy_sns_payload),
content_type="application/json",
HTTP_AUTHORIZATION=token,
HTTP_X_AMZ_SNS_MESSAGE_TYPE="Notification",
HTTP_X_AMZ_SNS_MESSAGE_ID=dummy_sns_message_id,
)
assert response.status_code == expected
@pytest.mark.parametrize(
"recipients,expected",
[
("{token}@example.com", status.HTTP_200_OK),
("{token}@example.com, another@example.com", status.HTTP_200_OK),
("{token}@example.com, another@example.com", status.HTTP_200_OK),
("{token}@other.com, {token}@example.com", status.HTTP_200_OK),
("{token}@other.com, {token}@another.com", status.HTTP_400_BAD_REQUEST),
],
)
@pytest.mark.django_db
def test_mailgun_provider_load(
settings, make_organization_and_user_with_token, make_alert_receive_channel, recipients, expected
):
settings.INBOUND_EMAIL_ESP = "mailgun"
settings.INBOUND_EMAIL_DOMAIN = "example.com"
settings.INBOUND_EMAIL_WEBHOOK_SECRET = "secret"
dummy_channel_token = "dummy-channel-token"
organization, _, token = make_organization_and_user_with_token()
_ = make_alert_receive_channel(organization, token=dummy_channel_token)
recipients = recipients.format(token=dummy_channel_token)
raw_event = {
"token": "06c96bafc3f42a66b9edd546347a2fe18dc23461fe80dc52f0",
"timestamp": "1461261330",
"signature": "dbb05e62be402448b36ffb81f6abfb888273c95617aa077b4da355b25bab3670",
"recipient": "{recipients}".format(recipients=recipients),
"sender": "envelope-from@example.org",
"body-mime": dedent(
"""\
From: sender@example.com
Subject: Dummy email message
To: {recipients}
Content-Type: text/plain
Hello!
--94eb2c05e174adb140055b6339c5--
""".format(
recipients=recipients
)
),
}
client = APIClient()
response = client.post(
reverse("integrations:inbound_email_webhook"),
data=raw_event,
HTTP_AUTHORIZATION=token,
)
assert response.status_code == expected
@pytest.mark.parametrize(
"sender_value,expected_result",
[
("'Alex Smith' <test@example.com>", "test@example.com"),
# double quotes required when including special characters
("\"'Alex Smith' via [TEST] mail\" <test@example.com>", "test@example.com"),
# missing double quotes
("'Alex Smith' via [TEST] mail <test@example.com>", "\"'Alex Smith' via\""),
],
)
def test_get_sender_from_email_message(sender_value, expected_result):
email = AnymailInboundMessage()
email["From"] = sender_value
view = InboundEmailWebhookView()
result = view.get_sender_from_email_message(email)
assert result == expected_result
@patch.object(create_alert, "delay")
@pytest.mark.django_db
def test_amazon_ses_pass(create_alert_mock, settings, make_organization, make_alert_receive_channel):
settings.INBOUND_EMAIL_ESP = "amazon_ses,mailgun"
settings.INBOUND_EMAIL_DOMAIN = "inbound.example.com"
settings.INBOUND_EMAIL_WEBHOOK_SECRET = "secret"
organization = make_organization()
alert_receive_channel = make_alert_receive_channel(
organization,
integration=AlertReceiveChannel.INTEGRATION_INBOUND_EMAIL,
token="test-token",
)
sns_payload, sns_headers, _ = _sns_inbound_email_setup(
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_200_OK
create_alert_mock.assert_called_once_with(
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,
},
received_at=ANY,
)
@patch.object(create_alert, "delay")
@httpretty.activate(verbose=True, allow_net_connect=True)
@pytest.mark.django_db
def test_amazon_ses_validated_s3_pass(mock_create_alert, 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
settings.INBOUND_EMAIL_AWS_ACCESS_KEY_ID = "test-access-key-id"
settings.INBOUND_EMAIL_AWS_SECRET_ACCESS_KEY = "test-secret-access-key"
settings.INBOUND_EMAIL_AWS_REGION = "us-east-2"
organization = make_organization()
alert_receive_channel = make_alert_receive_channel(
organization,
integration=AlertReceiveChannel.INTEGRATION_INBOUND_EMAIL,
token="test-token",
)
sns_payload, sns_headers, content = _sns_inbound_email_setup(
sender_email=SENDER_EMAIL,
to_email=TO_EMAIL,
subject=SUBJECT,
message=MESSAGE,
s3=True,
)
httpretty.register_uri(httpretty.GET, SIGNING_CERT_URL, body=CERTIFICATE)
httpretty.register_uri(
httpretty.HEAD,
"https://test-s3-bucket.s3.us-east-2.amazonaws.com/test-object-key",
responses=[httpretty.Response(body="")],
)
httpretty.register_uri(
httpretty.GET,
"https://test-s3-bucket.s3.us-east-2.amazonaws.com/test-object-key",
responses=[httpretty.Response(body=content)],
)
client = APIClient()
response = client.post(
reverse("integrations:inbound_email_webhook"),
data=sns_payload,
headers=sns_headers,
format="json",
)
assert response.status_code == status.HTTP_200_OK
mock_create_alert.assert_called_once_with(
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,
},
received_at=ANY,
)
assert len(httpretty.latest_requests()) == 3
assert (httpretty.latest_requests()[0].method, httpretty.latest_requests()[0].path) == (
"GET",
"/SimpleNotificationService-example.pem",
)
assert (httpretty.latest_requests()[1].method, httpretty.latest_requests()[1].path) == ("HEAD", "/test-object-key")
assert (httpretty.latest_requests()[2].method, httpretty.latest_requests()[2].path) == ("GET", "/test-object-key")
@patch("requests.get", return_value=Mock(content=CERTIFICATE))
@patch.object(create_alert, "delay")
@pytest.mark.django_db
def test_amazon_ses_validated_pass_html(
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()
alert_receive_channel = make_alert_receive_channel(
organization,
integration=AlertReceiveChannel.INTEGRATION_INBOUND_EMAIL,
token="test-token",
)
html_message = """\
<html>
<title>title</title>
<body>
<div>
<h1>h1</h1>
<br><br><br>
<p>p<b>b</b><i>i</i> <span>span</span></p> <p>new line</p> <hr>
<a href="https://example.com">link</a>
<ul>
<li>li1</li>
<li>li2</li>
</ul>
<table>
<tr>
<td>td1</td>
<td>td2</td>
</tr>
</table>
</div>
</body>
</html>
"""
plaintext_message = (
"title\n"
"h1\n"
"pbi span\n"
"new line\n"
"--------------------------------\n"
"link (https://example.com)\n"
"* li1\n"
"* li2\n"
"td1\n"
"td2"
)
sns_payload, sns_headers, _ = _sns_inbound_email_setup(
sender_email=SENDER_EMAIL,
to_email=TO_EMAIL,
subject=SUBJECT,
message=html_message,
content_type="text/html",
)
client = APIClient()
response = client.post(
reverse("integrations:inbound_email_webhook"),
data=sns_payload,
headers=sns_headers,
format="json",
)
assert response.status_code == status.HTTP_200_OK
mock_create_alert.assert_called_once_with(
title=SUBJECT,
message=plaintext_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": plaintext_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_setup(
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_setup(
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("requests.get", side_effect=RequestException)
@pytest.mark.django_db
def test_amazon_ses_validated_fail_cant_download_certificate(
_, 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_setup(
sender_email=SENDER_EMAIL,
to_email=TO_EMAIL,
subject=SUBJECT,
message=MESSAGE,
)
client = APIClient()
with pytest.raises(RequestException):
client.post(
reverse("integrations:inbound_email_webhook"),
data=sns_payload,
headers=sns_headers,
format="json",
)
@patch("requests.get", return_value=Mock(content=CERTIFICATE))
@pytest.mark.django_db
def test_amazon_ses_validated_caches_certificate(
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_setup(
sender_email=SENDER_EMAIL,
to_email=TO_EMAIL,
subject=SUBJECT,
message=MESSAGE,
)
client = APIClient()
for _ in range(2):
response = client.post(
reverse("integrations:inbound_email_webhook"),
data=sns_payload,
headers=sns_headers,
format="json",
)
assert response.status_code == status.HTTP_200_OK
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):
settings.INBOUND_EMAIL_ESP = "amazon_ses,mailgun"
settings.INBOUND_EMAIL_DOMAIN = "inbound.example.com"
settings.INBOUND_EMAIL_WEBHOOK_SECRET = "secret"
organization = make_organization()
alert_receive_channel = make_alert_receive_channel(
organization,
integration=AlertReceiveChannel.INTEGRATION_INBOUND_EMAIL,
token="test-token",
)
mailgun_payload = _mailgun_inbound_email_payload(
sender_email=SENDER_EMAIL,
to_email=TO_EMAIL,
subject=SUBJECT,
message=MESSAGE,
)
client = APIClient()
response = client.post(
reverse("integrations:inbound_email_webhook"),
data=mailgun_payload,
format="multipart",
)
assert response.status_code == status.HTTP_200_OK
create_alert_mock.assert_called_once_with(
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,
},
received_at=ANY,
)
@pytest.mark.django_db
def test_multiple_esps_fail(settings):
settings.INBOUND_EMAIL_ESP = "amazon_ses,mailgun"
settings.INBOUND_EMAIL_DOMAIN = "example.com"
settings.INBOUND_EMAIL_WEBHOOK_SECRET = "secret"
client = APIClient()
response = client.post(reverse("integrations:inbound_email_webhook"), data={})
assert response.status_code == status.HTTP_400_BAD_REQUEST