mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
Mailgun: disable non-ASCII attachment filename workaround when not needed
urllib3 v1.25 fixes non-ASCII filenames in multipart form data to be RFC 5758 compliant by default, so our earlier workaround is no longer needed. Disable the workaround if we detect that Requests is using a fixed version of urllib3. Closes #157
This commit is contained in:
@@ -52,6 +52,9 @@ matrix:
|
|||||||
- { env: TOXENV=django22-py37-none, python: 3.7 }
|
- { env: TOXENV=django22-py37-none, python: 3.7 }
|
||||||
- { env: TOXENV=django22-py37-amazon_ses, python: 3.7 }
|
- { env: TOXENV=django22-py37-amazon_ses, python: 3.7 }
|
||||||
- { env: TOXENV=django22-py37-sparkpost, python: 3.7 }
|
- { env: TOXENV=django22-py37-sparkpost, python: 3.7 }
|
||||||
|
# Test some specific older package versions
|
||||||
|
- { env: TOXENV=django111-py27-all-old_urllib3, python: 3.7 }
|
||||||
|
- { env: TOXENV=django22-py37-all-old_urllib3, python: 3.7 }
|
||||||
|
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- env: TOXENV=djangoMaster-py37-all
|
- env: TOXENV=djangoMaster-py37-all
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ Features
|
|||||||
(Thanks `@anstosa`_.)
|
(Thanks `@anstosa`_.)
|
||||||
|
|
||||||
|
|
||||||
|
Other
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
* **Mailgun:** Disable Anymail's workaround for a Requests/urllib3 issue with non-ASCII
|
||||||
|
attachment filenames when a newer version of urllib3--which fixes the problem--is
|
||||||
|
installed. (Workaround was added in Anymail v4.3; fix appears in urllib3 v1.25.)
|
||||||
|
|
||||||
|
|
||||||
v6.1
|
v6.1
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,21 @@ from ..utils import get_anymail_setting, rfc2822date
|
|||||||
from .base_requests import AnymailRequestsBackend, RequestsPayload
|
from .base_requests import AnymailRequestsBackend, RequestsPayload
|
||||||
|
|
||||||
|
|
||||||
|
# Feature-detect whether requests (urllib3) correctly uses RFC 7578 encoding for non-
|
||||||
|
# ASCII filenames in Content-Disposition headers. (This was fixed in urllib3 v1.25.)
|
||||||
|
# See MailgunPayload.get_request_params for info (and a workaround on older versions).
|
||||||
|
# (Note: when this workaround is removed, please also remove the "old_urllib3" tox envs.)
|
||||||
|
def is_requests_rfc_5758_compliant():
|
||||||
|
request = Request(method='POST', url='https://www.example.com',
|
||||||
|
files=[('attachment', (u'\N{NOT SIGN}.txt', 'test', 'text/plain'))])
|
||||||
|
prepared = request.prepare()
|
||||||
|
form_data = prepared.body # bytes
|
||||||
|
return b'filename*=' not in form_data
|
||||||
|
|
||||||
|
|
||||||
|
REQUESTS_IS_RFC_7578_COMPLIANT = is_requests_rfc_5758_compliant()
|
||||||
|
|
||||||
|
|
||||||
class EmailBackend(AnymailRequestsBackend):
|
class EmailBackend(AnymailRequestsBackend):
|
||||||
"""
|
"""
|
||||||
Mailgun API Email Backend
|
Mailgun API Email Backend
|
||||||
@@ -94,7 +109,7 @@ class MailgunPayload(RequestsPayload):
|
|||||||
non_ascii_filenames = [filename
|
non_ascii_filenames = [filename
|
||||||
for (field, (filename, content, mimetype)) in params["files"]
|
for (field, (filename, content, mimetype)) in params["files"]
|
||||||
if filename is not None and not isascii(filename)]
|
if filename is not None and not isascii(filename)]
|
||||||
if non_ascii_filenames:
|
if non_ascii_filenames and not REQUESTS_IS_RFC_7578_COMPLIANT:
|
||||||
# Workaround https://github.com/requests/requests/issues/4652:
|
# Workaround https://github.com/requests/requests/issues/4652:
|
||||||
# Mailgun expects RFC 7578 compliant multipart/form-data, and is confused
|
# Mailgun expects RFC 7578 compliant multipart/form-data, and is confused
|
||||||
# by Requests/urllib3's improper use of RFC 2231 encoded filename parameters
|
# by Requests/urllib3's improper use of RFC 2231 encoded filename parameters
|
||||||
|
|||||||
@@ -115,6 +115,13 @@ class RequestsBackendMockAPITestCase(SimpleTestCase, AnymailTestMixin):
|
|||||||
"""Returns the auth sent to the mock ESP API"""
|
"""Returns the auth sent to the mock ESP API"""
|
||||||
return self.get_api_call_arg('auth', required)
|
return self.get_api_call_arg('auth', required)
|
||||||
|
|
||||||
|
def get_api_prepared_request(self):
|
||||||
|
"""Returns the PreparedRequest that would have been sent"""
|
||||||
|
(args, kwargs) = self.mock_request.call_args
|
||||||
|
kwargs.pop('timeout', None) # Session-only param
|
||||||
|
request = requests.Request(**kwargs)
|
||||||
|
return request.prepare()
|
||||||
|
|
||||||
def assert_esp_not_called(self, msg=None):
|
def assert_esp_not_called(self, msg=None):
|
||||||
if self.mock_request.called:
|
if self.mock_request.called:
|
||||||
raise AssertionError(msg or "ESP API was called and shouldn't have been")
|
raise AssertionError(msg or "ESP API was called and shouldn't have been")
|
||||||
|
|||||||
@@ -182,12 +182,20 @@ class MailgunBackendStandardEmailTests(MailgunBackendMockAPITestCase):
|
|||||||
self.message.send()
|
self.message.send()
|
||||||
|
|
||||||
# Verify the RFC 7578 compliance workaround has kicked in:
|
# Verify the RFC 7578 compliance workaround has kicked in:
|
||||||
data = self.get_api_call_data().decode("utf-8")
|
data = self.get_api_call_data()
|
||||||
|
if isinstance(data, dict): # workaround not needed or used (but let's double check actual request)
|
||||||
|
workaround = False
|
||||||
|
prepared = self.get_api_prepared_request()
|
||||||
|
data = prepared.body
|
||||||
|
else:
|
||||||
|
workaround = True
|
||||||
|
data = data.decode("utf-8").replace("\r\n", "\n")
|
||||||
self.assertNotIn("filename*=", data) # No RFC 2231 encoding
|
self.assertNotIn("filename*=", data) # No RFC 2231 encoding
|
||||||
self.assertIn(u'Content-Disposition: form-data; name="attachment"; filename="Une pièce jointe.html"', data)
|
self.assertIn(u'Content-Disposition: form-data; name="attachment"; filename="Une pièce jointe.html"', data)
|
||||||
|
|
||||||
files = self.get_api_call_files(required=False)
|
if workaround:
|
||||||
self.assertFalse(files) # files should have been moved to formdata body
|
files = self.get_api_call_files(required=False)
|
||||||
|
self.assertFalse(files) # files should have been moved to formdata body
|
||||||
|
|
||||||
def test_rfc_7578_compliance(self):
|
def test_rfc_7578_compliance(self):
|
||||||
# Check some corner cases in the workaround that undoes RFC 2231 multipart/form-data encoding...
|
# Check some corner cases in the workaround that undoes RFC 2231 multipart/form-data encoding...
|
||||||
@@ -214,7 +222,12 @@ class MailgunBackendStandardEmailTests(MailgunBackendMockAPITestCase):
|
|||||||
self.message.attach(u"besked med vedhæftede filer", forwarded_message, "message/rfc822")
|
self.message.attach(u"besked med vedhæftede filer", forwarded_message, "message/rfc822")
|
||||||
self.message.send()
|
self.message.send()
|
||||||
|
|
||||||
data = self.get_api_call_data().decode("utf-8").replace("\r\n", "\n")
|
data = self.get_api_call_data()
|
||||||
|
if isinstance(data, dict): # workaround not needed or used (but let's double check actual request)
|
||||||
|
prepared = self.get_api_prepared_request()
|
||||||
|
data = prepared.body
|
||||||
|
data = data.decode("utf-8").replace("\r\n", "\n")
|
||||||
|
|
||||||
# Top-level attachment (in form-data) should have RFC 7578 filename (raw Unicode):
|
# Top-level attachment (in form-data) should have RFC 7578 filename (raw Unicode):
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
u'Content-Disposition: form-data; name="attachment"; filename="besked med vedhæftede filer"', data)
|
u'Content-Disposition: form-data; name="attachment"; filename="besked med vedhæftede filer"', data)
|
||||||
|
|||||||
4
tox.ini
4
tox.ini
@@ -15,6 +15,9 @@ envlist =
|
|||||||
djangoMaster-py{36,37}-all
|
djangoMaster-py{36,37}-all
|
||||||
# ... then partial installation (limit extras):
|
# ... then partial installation (limit extras):
|
||||||
django22-py37-{none,amazon_ses,sparkpost}
|
django22-py37-{none,amazon_ses,sparkpost}
|
||||||
|
# ... then older versions of some dependencies:
|
||||||
|
django111-py27-all-old_urllib3
|
||||||
|
django22-py37-all-old_urllib3
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
@@ -23,6 +26,7 @@ deps =
|
|||||||
django21: django~=2.1.0
|
django21: django~=2.1.0
|
||||||
django22: django~=2.2.0
|
django22: django~=2.2.0
|
||||||
djangoMaster: https://github.com/django/django/tarball/master
|
djangoMaster: https://github.com/django/django/tarball/master
|
||||||
|
old_urllib3: urllib3<1.25
|
||||||
# testing dependencies (duplicates setup.py tests_require, less optional extras):
|
# testing dependencies (duplicates setup.py tests_require, less optional extras):
|
||||||
mock
|
mock
|
||||||
extras =
|
extras =
|
||||||
|
|||||||
Reference in New Issue
Block a user