From dabbdad3bdb148be530fcb043ac93861c5368706 Mon Sep 17 00:00:00 2001 From: medmunds Date: Sat, 23 Feb 2019 15:27:41 -0800 Subject: [PATCH] Properly encode path components used to construct API URLs Resolves #144 and similar potential issues --- anymail/backends/mailgun.py | 8 ++++---- anymail/backends/mailjet.py | 4 +++- anymail/backends/sendinblue.py | 3 ++- tests/test_mailgun_backend.py | 11 +++++++++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/anymail/backends/mailgun.py b/anymail/backends/mailgun.py index 49536c4..2af8cc5 100644 --- a/anymail/backends/mailgun.py +++ b/anymail/backends/mailgun.py @@ -1,6 +1,6 @@ from datetime import datetime from email.utils import encode_rfc2231 -from six.moves.urllib.parse import quote_plus +from six.moves.urllib.parse import quote from requests import Request @@ -82,12 +82,12 @@ class MailgunPayload(RequestsPayload): "Either provide valid `from_email`, " "or set `message.esp_extra={'sender_domain': 'example.com'}`", backend=self.backend, email_message=self.message, payload=self) - if '/' in self.sender_domain or '%' in self.sender_domain: + if '/' in self.sender_domain or '%2f' in self.sender_domain.lower(): # Mailgun returns a cryptic 200-OK "Mailgun Magnificent API" response # if '/' (or even %-encoded '/') confuses it about the API endpoint. - raise AnymailError("Invalid sender domain '%s'" % self.sender_domain, + raise AnymailError("Invalid '/' in sender domain '%s'" % self.sender_domain, backend=self.backend, email_message=self.message, payload=self) - return "%s/messages" % quote_plus(self.sender_domain) + return "%s/messages" % quote(self.sender_domain, safe='') def get_request_params(self, api_url): params = super(MailgunPayload, self).get_request_params(api_url) diff --git a/anymail/backends/mailjet.py b/anymail/backends/mailjet.py index 0e873fb..12d57a1 100644 --- a/anymail/backends/mailjet.py +++ b/anymail/backends/mailjet.py @@ -1,3 +1,5 @@ +from six.moves.urllib.parse import quote + from ..exceptions import AnymailRequestsAPIError from ..message import AnymailRecipientStatus, ANYMAIL_STATUSES from ..utils import get_anymail_setting, EmailAddress, parse_address_list @@ -131,7 +133,7 @@ class MailjetPayload(RequestsPayload): template_id = self.data.get("Mj-TemplateID") if template_id and not self.data.get("FromEmail"): response = self.backend.session.get( - "%sREST/template/%s/detailcontent" % (self.backend.api_url, template_id), + "%sREST/template/%s/detailcontent" % (self.backend.api_url, quote(str(template_id), safe='')), auth=self.auth, timeout=self.backend.timeout ) self.backend.raise_for_status(response, None, self.message) diff --git a/anymail/backends/sendinblue.py b/anymail/backends/sendinblue.py index 0805457..35caf3c 100644 --- a/anymail/backends/sendinblue.py +++ b/anymail/backends/sendinblue.py @@ -1,4 +1,5 @@ from requests.structures import CaseInsensitiveDict +from six.moves.urllib.parse import quote from .base_requests import AnymailRequestsBackend, RequestsPayload from ..exceptions import AnymailRequestsAPIError @@ -76,7 +77,7 @@ class SendinBluePayload(RequestsPayload): def get_api_endpoint(self): if self.template_id: - return "smtp/templates/%s/send" % self.template_id + return "smtp/templates/%s/send" % quote(str(self.template_id), safe='') else: return "smtp/email" diff --git a/tests/test_mailgun_backend.py b/tests/test_mailgun_backend.py index 842f2d7..d42dda4 100644 --- a/tests/test_mailgun_backend.py +++ b/tests/test_mailgun_backend.py @@ -565,7 +565,7 @@ class MailgunBackendAnymailFeatureTests(MailgunBackendMockAPITestCase): # (which returns a cryptic 200-OK "Mailgun Magnificent API" response). self.message.from_email = "" with self.assertRaisesMessage(AnymailError, - "Invalid sender domain 'example.com/invalid'"): + "Invalid '/' in sender domain 'example.com/invalid'"): self.message.send() @override_settings(ANYMAIL_MAILGUN_SENDER_DOMAIN='example.com%2Finvalid') @@ -573,9 +573,16 @@ class MailgunBackendAnymailFeatureTests(MailgunBackendMockAPITestCase): # See previous test. Also, note that Mailgun unquotes % encoding *before* # extracting the sender domain (so %2f is just as bad as '/') with self.assertRaisesMessage(AnymailError, - "Invalid sender domain 'example.com%2Finvalid'"): + "Invalid '/' in sender domain 'example.com%2Finvalid'"): self.message.send() + @override_settings(ANYMAIL_MAILGUN_SENDER_DOMAIN='example.com # oops') + def test_encode_sender_domain(self): + # See previous tests. For anything other than slashes, we let Mailgun detect + # the problem (but must properly encode the domain in the API URL) + self.message.send() + self.assert_esp_called('/example.com%20%23%20oops/messages') + def test_default_omits_options(self): """Make sure by default we don't send any ESP-specific options.