Unify SMTP2GO email and SMS into one webhook URL.
Deploy Beta / unit-tests (push) Successful in 13s
Deploy Beta / docker (push) Successful in 18s
Deploy Beta / deploy-beta (push) Successful in 1m38s

Classify payloads on /webhooks/smtp2go/ so one SMTP2GO webhook covers both channels under the 10-webhook limit; keep /email/ and /sms/ as aliases and harden SMS event matching.
This commit is contained in:
2026-08-10 11:06:37 -05:00
parent f7f3174e1c
commit ee28dcabab
7 changed files with 472 additions and 115 deletions
+253
View File
@@ -630,6 +630,116 @@ class Smtp2goSmsWebhookTests(TestCase):
self.message.refresh_from_db()
self.assertEqual(self.message.status, Message.Status.FAILED)
def test_sms_short_event_name_delivered(self):
"""API sms_events use short names; body may omit sms_ prefix."""
url = reverse("messaging:sms_webhook")
import json
response = self.client.post(
url,
data=json.dumps(
{
"event": "delivered",
"message_id": "sms-provider-99",
"destination_number": "5550142291",
}
),
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
self.message.refresh_from_db()
self.assertEqual(self.message.status, Message.Status.DELIVERED)
self.assertTrue(
ProviderEvent.objects.filter(
message=self.message, event_type="sms_delivered"
).exists()
)
def test_sms_ui_label_and_form_encoded(self):
url = reverse("messaging:sms_webhook")
self.message.status = Message.Status.QUEUED
self.message.save(update_fields=["status"])
response = self.client.post(
url,
data={
"event": "Submitted",
"message_id": "sms-provider-99",
"destination_number": "5550142291",
},
)
self.assertEqual(response.status_code, 200)
self.message.refresh_from_db()
self.assertEqual(self.message.status, Message.Status.SENT)
self.assertTrue(
ProviderEvent.objects.filter(
message=self.message, event_type="sms_submitted"
).exists()
)
def test_sms_rejected_marks_failed(self):
url = reverse("messaging:sms_webhook")
import json
response = self.client.post(
url,
data=json.dumps(
{
"event": "sms_rejected",
"message_id": "sms-provider-99",
"destination_number": "5550142291",
"status_code": "blocked",
}
),
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
self.message.refresh_from_db()
self.assertEqual(self.message.status, Message.Status.FAILED)
def test_sms_webhook_id_is_not_used_as_message_id(self):
"""Docs: `id` is the webhook notification id, not the SMS id."""
url = reverse("messaging:sms_webhook")
import json
response = self.client.post(
url,
data=json.dumps(
{
"event": "sms_delivered",
"id": "sms-provider-99",
"destination_number": "19999999999",
}
),
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
self.message.refresh_from_db()
self.assertEqual(self.message.status, Message.Status.SENT)
self.assertTrue(
ProviderEvent.objects.filter(
message=None, event_type="sms_delivered"
).exists()
)
def test_sms_opt_out_unmatched_suppresses_by_phone(self):
url = reverse("messaging:sms_webhook")
import json
self.message.delete()
response = self.client.post(
url,
data=json.dumps(
{
"event": "opt-out",
"message_id": "unknown",
"destination_number": "+15550142291",
}
),
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
self.assertFalse(contact_may_receive(self.contact, Channel.SMS))
def test_inbound_stop_still_works(self):
url = reverse("messaging:sms_webhook")
response = self.client.post(
@@ -638,6 +748,14 @@ class Smtp2goSmsWebhookTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertFalse(contact_may_receive(self.contact, Channel.SMS))
def test_inbound_unsub_keyword(self):
url = reverse("messaging:sms_webhook")
response = self.client.post(
url, {"from": "5550142291", "text": "UNSUB"}
)
self.assertEqual(response.status_code, 200)
self.assertFalse(contact_may_receive(self.contact, Channel.SMS))
def test_sms_webhook_requires_bearer_when_secret_set(self):
url = reverse("messaging:sms_webhook")
import json
@@ -663,6 +781,141 @@ class Smtp2goSmsWebhookTests(TestCase):
self.assertEqual(ok.status_code, 200)
class UnifiedSmtp2goWebhookTests(TestCase):
def setUp(self):
self.client = Client()
self.email_contact = Contact.objects.create(
email="pat@example.com",
first_name="Pat",
)
set_channel_consent(
self.email_contact, Channel.EMAIL, opted_in=True, reason="test"
)
self.email_campaign = create_campaign_draft(
name="Unified email",
audience=Campaign.Audience.EMAIL_OPT_IN,
subject="Hello",
body="Body",
)
self.email_message = self.email_campaign.messages.get()
self.email_message.status = Message.Status.SENT
self.email_message.save(update_fields=["status"])
self.sms_contact = Contact.objects.create(
email="sms.pat@example.com",
phone="+15550142291",
first_name="Pat",
)
set_channel_consent(
self.sms_contact, Channel.SMS, opted_in=True, reason="test"
)
self.sms_campaign = create_campaign_draft(
name="Unified SMS",
audience=Campaign.Audience.SMS_OPT_IN,
subject="",
body="Hi",
)
self.sms_message = self.sms_campaign.messages.get()
self.sms_message.status = Message.Status.SENT
self.sms_message.provider_message_id = "sms-unified-1"
self.sms_message.save()
def test_classify_helpers(self):
from messaging.webhooks import classify_smtp2go_payload
self.assertEqual(
classify_smtp2go_payload({"from": "555", "text": "STOP"}),
"sms_inbound",
)
self.assertEqual(
classify_smtp2go_payload(
{"event": "sms_delivered", "message_id": "x"}
),
"sms",
)
self.assertEqual(
classify_smtp2go_payload(
{
"event": "delivered",
"destination_number": "5550142291",
"message_id": "x",
}
),
"sms",
)
self.assertEqual(
classify_smtp2go_payload(
{
"event": "delivered",
"rcpt": "pat@example.com",
"email_id": "e1",
}
),
"email",
)
self.assertEqual(
classify_smtp2go_payload({"event": "click", "rcpt": "a@b.c"}),
"email",
)
def test_unified_url_routes_email_and_sms(self):
import json
url = reverse("messaging:smtp2go_webhook")
email_resp = self.client.post(
url,
data=json.dumps(
{
"event": "click",
"rcpt": "pat@example.com",
"X-Monica-Message-Id": str(self.email_message.pk),
}
),
content_type="application/json",
)
self.assertEqual(email_resp.status_code, 200)
self.assertEqual(email_resp.json()["channel"], "email")
self.email_message.refresh_from_db()
self.assertEqual(self.email_message.status, Message.Status.CLICKED)
sms_resp = self.client.post(
url,
data=json.dumps(
{
"event": "sms_delivered",
"message_id": "sms-unified-1",
"destination_number": "5550142291",
}
),
content_type="application/json",
)
self.assertEqual(sms_resp.status_code, 200)
self.assertEqual(sms_resp.json()["channel"], "sms")
self.sms_message.refresh_from_db()
self.assertEqual(self.sms_message.status, Message.Status.DELIVERED)
def test_legacy_email_path_still_accepts_sms(self):
"""Legacy /email/ alias is the unified handler."""
import json
url = reverse("messaging:email_webhook")
response = self.client.post(
url,
data=json.dumps(
{
"event": "delivered",
"message_id": "sms-unified-1",
"destination_number": "5550142291",
}
),
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["channel"], "sms")
self.sms_message.refresh_from_db()
self.assertEqual(self.sms_message.status, Message.Status.DELIVERED)
class PcmPostcardWebhookTests(TestCase):
def setUp(self):
self.client = Client()