mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Drop support for Python 3.6 and old urllib3
This commit is contained in:
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@@ -57,8 +57,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
python -VV
|
python -VV
|
||||||
# Must pin virtualenv for tox py36 testenv:
|
python -m pip install 'tox<4'
|
||||||
python -m pip install 'tox<4' 'virtualenv<20.22.0'
|
|
||||||
python -m tox --version
|
python -m tox --version
|
||||||
- name: Test ${{ matrix.tox.name }}
|
- name: Test ${{ matrix.tox.name }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ Release history
|
|||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^
|
||||||
.. This extra heading level keeps the ToC from becoming unmanageably long
|
.. This extra heading level keeps the ToC from becoming unmanageably long
|
||||||
|
|
||||||
|
vNext
|
||||||
|
-----
|
||||||
|
|
||||||
|
*unreleased changes*
|
||||||
|
|
||||||
|
Breaking changes
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* Require Python 3.7 or later.
|
||||||
|
* Require urllib3 1.25 or later (released 2019-04-29).
|
||||||
|
|
||||||
|
|
||||||
v9.2
|
v9.2
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,12 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from email.utils import encode_rfc2231
|
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from requests import Request
|
|
||||||
|
|
||||||
from ..exceptions import AnymailError, AnymailRequestsAPIError
|
from ..exceptions import AnymailError, AnymailRequestsAPIError
|
||||||
from ..message import AnymailRecipientStatus
|
from ..message import AnymailRecipientStatus
|
||||||
from ..utils import get_anymail_setting, rfc2822date
|
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 "old_urllib3" tox envs.)
|
|
||||||
def is_requests_rfc_5758_compliant():
|
|
||||||
request = Request(
|
|
||||||
method="POST",
|
|
||||||
url="https://www.example.com",
|
|
||||||
files=[("attachment", ("\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
|
||||||
@@ -163,37 +142,6 @@ class MailgunPayload(RequestsPayload):
|
|||||||
)
|
)
|
||||||
return "%s/messages" % quote(self.sender_domain, safe="")
|
return "%s/messages" % quote(self.sender_domain, safe="")
|
||||||
|
|
||||||
def get_request_params(self, api_url):
|
|
||||||
params = super().get_request_params(api_url)
|
|
||||||
non_ascii_filenames = [
|
|
||||||
filename
|
|
||||||
for (field, (filename, content, mimetype)) in params["files"]
|
|
||||||
if filename is not None and not isascii(filename)
|
|
||||||
]
|
|
||||||
if non_ascii_filenames and not REQUESTS_IS_RFC_7578_COMPLIANT:
|
|
||||||
# Workaround https://github.com/requests/requests/issues/4652:
|
|
||||||
# Mailgun expects RFC 7578 compliant multipart/form-data, and is confused
|
|
||||||
# by Requests/urllib3's improper use of RFC 2231 encoded filename parameters
|
|
||||||
# ("filename*=utf-8''...") in Content-Disposition headers.
|
|
||||||
# The workaround is to pre-generate the (non-compliant) form-data body, and
|
|
||||||
# replace 'filename*={RFC 2231 encoded}' with 'filename="{UTF-8 bytes}"'.
|
|
||||||
# Replace _only_ filenames that will be problems (not all "filename*=...")
|
|
||||||
# to minimize potential side effects--e.g., in attached messages that might
|
|
||||||
# have their own attachments with (correctly) RFC 2231 encoded filenames.
|
|
||||||
prepared = Request(**params).prepare()
|
|
||||||
form_data = prepared.body # bytes
|
|
||||||
for filename in non_ascii_filenames: # text
|
|
||||||
rfc2231_filename = encode_rfc2231(filename, charset="utf-8")
|
|
||||||
form_data = form_data.replace(
|
|
||||||
b"filename*=" + rfc2231_filename.encode("utf-8"),
|
|
||||||
b'filename="' + filename.encode("utf-8") + b'"',
|
|
||||||
)
|
|
||||||
params["data"] = form_data
|
|
||||||
# Content-Type: multipart/form-data; boundary=...
|
|
||||||
params["headers"]["Content-Type"] = prepared.headers["Content-Type"]
|
|
||||||
params["files"] = None # these are now in the form_data body
|
|
||||||
return params
|
|
||||||
|
|
||||||
def serialize_data(self):
|
def serialize_data(self):
|
||||||
self.populate_recipient_variables()
|
self.populate_recipient_variables()
|
||||||
return self.data
|
return self.data
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[tool.black]
|
[tool.black]
|
||||||
force-exclude = '^/tests/test_settings/settings_.*\.py'
|
force-exclude = '^/tests/test_settings/settings_.*\.py'
|
||||||
max-line-length = 88
|
max-line-length = 88
|
||||||
target-version = ["py36"]
|
target-version = ["py37"]
|
||||||
|
|
||||||
[tool.doc8]
|
[tool.doc8]
|
||||||
# ignore very long lines in ESP support table:
|
# ignore very long lines in ESP support table:
|
||||||
@@ -16,4 +16,4 @@ max-line-length = 120
|
|||||||
combine_as_imports = true
|
combine_as_imports = true
|
||||||
known_first_party = "anymail"
|
known_first_party = "anymail"
|
||||||
profile = "black"
|
profile = "black"
|
||||||
py_version = "36"
|
py_version = "37"
|
||||||
|
|||||||
10
setup.py
10
setup.py
@@ -45,7 +45,6 @@ requirements_dev = [
|
|||||||
"sphinx-rtd-theme",
|
"sphinx-rtd-theme",
|
||||||
"tox",
|
"tox",
|
||||||
"twine",
|
"twine",
|
||||||
"virtualenv<20.22.0", # tox dependency, pinned for Python 3.6 tox testenv
|
|
||||||
"wheel",
|
"wheel",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -71,8 +70,12 @@ setup(
|
|||||||
license="BSD License",
|
license="BSD License",
|
||||||
packages=["anymail"],
|
packages=["anymail"],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
python_requires=">=3.6",
|
python_requires=">=3.7",
|
||||||
install_requires=["django>=2.0", "requests>=2.4.3"],
|
install_requires=[
|
||||||
|
"django>=2.0",
|
||||||
|
"requests>=2.4.3",
|
||||||
|
"urllib3>=1.25.0", # requests dependency: fixes RFC 7578 header encoding
|
||||||
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
# This can be used if particular backends have unique dependencies.
|
# This can be used if particular backends have unique dependencies.
|
||||||
# For simplicity, requests is included in the base requirements.
|
# For simplicity, requests is included in the base requirements.
|
||||||
@@ -100,7 +103,6 @@ setup(
|
|||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.6",
|
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
|
|||||||
17
tox.ini
17
tox.ini
@@ -6,7 +6,7 @@ envlist =
|
|||||||
# Test lint, docs, earliest/latest Django first, to catch most errors early...
|
# Test lint, docs, earliest/latest Django first, to catch most errors early...
|
||||||
lint
|
lint
|
||||||
django42-py311-all
|
django42-py311-all
|
||||||
django30-py36-all
|
django30-py37-all
|
||||||
docs
|
docs
|
||||||
# ... then test all the other supported combinations:
|
# ... then test all the other supported combinations:
|
||||||
# Django 4.2: Python 3.8, 3.9, 3.10, 3.11
|
# Django 4.2: Python 3.8, 3.9, 3.10, 3.11
|
||||||
@@ -15,12 +15,12 @@ envlist =
|
|||||||
django41-py{38,39,310,py38,py39}-all
|
django41-py{38,39,310,py38,py39}-all
|
||||||
# Django 4.0: Python 3.8, 3.9, 3.10
|
# Django 4.0: Python 3.8, 3.9, 3.10
|
||||||
django40-py{38,39,310,py38,py39}-all
|
django40-py{38,39,310,py38,py39}-all
|
||||||
# Django 3.2: Python 3.6, 3.7, 3.8, 3.9
|
# Django 3.2: Python 3.6 (eol 2021-12-23), 3.7, 3.8, 3.9
|
||||||
django32-py{36,37,38,39,py38,py39}-all
|
django32-py{37,38,39,py38,py39}-all
|
||||||
# Django 3.1: Python 3.6, 3.7, 3.8, 3.9 (added in 3.1.3)
|
# Django 3.1: Python 3.6 (eol 2021-12-23), 3.7, 3.8, 3.9 (added in 3.1.3)
|
||||||
django31-py{36,37,38,39,py38,py39}-all
|
django31-py{37,38,39,py38,py39}-all
|
||||||
# Django 3.0: Python 3.6, 3.7, 3.8, 3.9 (added in 3.0.11)
|
# Django 3.0: Python 3.6 (eol 2021-12-23), 3.7, 3.8, 3.9 (added in 3.0.11)
|
||||||
django30-py{37,38,39,py38,py39}-all
|
django30-py{38,39,py38,py39}-all
|
||||||
# ... then prereleases (if available) and current development:
|
# ... then prereleases (if available) and current development:
|
||||||
# Django 5.0 alpha: Python 3.10+
|
# Django 5.0 alpha: Python 3.10+
|
||||||
# [not yet in alpha] django50-py{310,311,py310,py311}-all
|
# [not yet in alpha] django50-py{310,311,py310,py311}-all
|
||||||
@@ -28,8 +28,6 @@ envlist =
|
|||||||
djangoDev-py{310,311}-all
|
djangoDev-py{310,311}-all
|
||||||
# ... then partial installation (limit extras):
|
# ... then partial installation (limit extras):
|
||||||
django42-py311-{none,amazon_ses,postal}
|
django42-py311-{none,amazon_ses,postal}
|
||||||
# ... then older versions of some dependencies:
|
|
||||||
django32-py37-all-old_urllib3
|
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
@@ -41,7 +39,6 @@ deps =
|
|||||||
django42: django~=4.2.0
|
django42: django~=4.2.0
|
||||||
django50: django~=5.0.0a0
|
django50: django~=5.0.0a0
|
||||||
djangoDev: https://github.com/django/django/tarball/main
|
djangoDev: https://github.com/django/django/tarball/main
|
||||||
old_urllib3: urllib3<1.25
|
|
||||||
extras =
|
extras =
|
||||||
# install [test] extras, unconditionally
|
# install [test] extras, unconditionally
|
||||||
test
|
test
|
||||||
|
|||||||
Reference in New Issue
Block a user