Properly encode path components used to construct API URLs

Resolves #144 and similar potential issues
This commit is contained in:
medmunds
2019-02-23 15:27:41 -08:00
parent 578bad9a57
commit dabbdad3bd
4 changed files with 18 additions and 8 deletions

View File

@@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from email.utils import encode_rfc2231 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 from requests import Request
@@ -82,12 +82,12 @@ class MailgunPayload(RequestsPayload):
"Either provide valid `from_email`, " "Either provide valid `from_email`, "
"or set `message.esp_extra={'sender_domain': 'example.com'}`", "or set `message.esp_extra={'sender_domain': 'example.com'}`",
backend=self.backend, email_message=self.message, payload=self) 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 # Mailgun returns a cryptic 200-OK "Mailgun Magnificent API" response
# if '/' (or even %-encoded '/') confuses it about the API endpoint. # 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) 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): def get_request_params(self, api_url):
params = super(MailgunPayload, self).get_request_params(api_url) params = super(MailgunPayload, self).get_request_params(api_url)

View File

@@ -1,3 +1,5 @@
from six.moves.urllib.parse import quote
from ..exceptions import AnymailRequestsAPIError from ..exceptions import AnymailRequestsAPIError
from ..message import AnymailRecipientStatus, ANYMAIL_STATUSES from ..message import AnymailRecipientStatus, ANYMAIL_STATUSES
from ..utils import get_anymail_setting, EmailAddress, parse_address_list from ..utils import get_anymail_setting, EmailAddress, parse_address_list
@@ -131,7 +133,7 @@ class MailjetPayload(RequestsPayload):
template_id = self.data.get("Mj-TemplateID") template_id = self.data.get("Mj-TemplateID")
if template_id and not self.data.get("FromEmail"): if template_id and not self.data.get("FromEmail"):
response = self.backend.session.get( 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 auth=self.auth, timeout=self.backend.timeout
) )
self.backend.raise_for_status(response, None, self.message) self.backend.raise_for_status(response, None, self.message)

View File

@@ -1,4 +1,5 @@
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from six.moves.urllib.parse import quote
from .base_requests import AnymailRequestsBackend, RequestsPayload from .base_requests import AnymailRequestsBackend, RequestsPayload
from ..exceptions import AnymailRequestsAPIError from ..exceptions import AnymailRequestsAPIError
@@ -76,7 +77,7 @@ class SendinBluePayload(RequestsPayload):
def get_api_endpoint(self): def get_api_endpoint(self):
if self.template_id: if self.template_id:
return "smtp/templates/%s/send" % self.template_id return "smtp/templates/%s/send" % quote(str(self.template_id), safe='')
else: else:
return "smtp/email" return "smtp/email"

View File

@@ -565,7 +565,7 @@ class MailgunBackendAnymailFeatureTests(MailgunBackendMockAPITestCase):
# (which returns a cryptic 200-OK "Mailgun Magnificent API" response). # (which returns a cryptic 200-OK "Mailgun Magnificent API" response).
self.message.from_email = "<from@example.com/invalid>" self.message.from_email = "<from@example.com/invalid>"
with self.assertRaisesMessage(AnymailError, with self.assertRaisesMessage(AnymailError,
"Invalid sender domain 'example.com/invalid'"): "Invalid '/' in sender domain 'example.com/invalid'"):
self.message.send() self.message.send()
@override_settings(ANYMAIL_MAILGUN_SENDER_DOMAIN='example.com%2Finvalid') @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* # See previous test. Also, note that Mailgun unquotes % encoding *before*
# extracting the sender domain (so %2f is just as bad as '/') # extracting the sender domain (so %2f is just as bad as '/')
with self.assertRaisesMessage(AnymailError, with self.assertRaisesMessage(AnymailError,
"Invalid sender domain 'example.com%2Finvalid'"): "Invalid '/' in sender domain 'example.com%2Finvalid'"):
self.message.send() 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): def test_default_omits_options(self):
"""Make sure by default we don't send any ESP-specific options. """Make sure by default we don't send any ESP-specific options.