diff --git a/.github/workflows/messages-ghcr.yml b/.github/workflows/messages-ghcr.yml index 0016b54e..2d68874b 100644 --- a/.github/workflows/messages-ghcr.yml +++ b/.github/workflows/messages-ghcr.yml @@ -9,7 +9,7 @@ name: Build and publish OCI images jobs: - # Shared base (debian-trixie + uv + managed CPython 3.14.6), built + pushed once + # Shared base (debian-trixie + uv + managed CPython), built + pushed once # from deploy/python-uv/Dockerfile, then referenced by digest via # PYTHON_UV_IMAGE in the backend and MTA builds below. docker-publish-python-uv: diff --git a/.github/workflows/messages.yml b/.github/workflows/messages.yml index d5fb3430..1dfe0291 100644 --- a/.github/workflows/messages.yml +++ b/.github/workflows/messages.yml @@ -44,6 +44,50 @@ jobs: run: make typecheck-jmap-email + lint-jmap-email: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Create env files + run: make create-env-files + - name: Run ruff + pylint on the jmap-email library + run: make lint-jmap-email + + + test-jmap-email: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Create env files + run: make create-env-files + - name: Run jmap-email tests + run: make test-jmap-email + + + fuzz-jmap-email: + runs-on: ubuntu-latest + env: + # `-m fuzz` is excluded from the default selection, so it only ever + # runs here. Full sweep on main; a lighter one on pull requests to + # keep the job to a few minutes. + FUZZ_EXAMPLES: ${{ github.event_name == 'pull_request' && '2000' || '10000' }} + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Create env files + run: make create-env-files + - name: Run the jmap-email Hypothesis fuzz suite + run: make fuzz-jmap-email + + test-back: runs-on: ubuntu-latest env: diff --git a/bin/release-jmap-email.sh b/bin/release-jmap-email.sh index ea69b1e6..68d4fd77 100755 --- a/bin/release-jmap-email.sh +++ b/bin/release-jmap-email.sh @@ -2,7 +2,7 @@ # # Interactive PyPI release for the jmap-email package. # -# Hermetic: every step runs inside the python:3.14.6-slim image, so a +# Hermetic: every step runs inside the python slim image, so a # clean VM only needs Docker. The host never touches pip or twine. # # Flow: @@ -21,7 +21,7 @@ set -eo pipefail REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PKG_DIR="${REPO_DIR}/src/jmap-email" -PYTHON_IMAGE="python:3.14.6-slim" +PYTHON_IMAGE="python:3.14.7-slim" BOLD=$'\033[1m' GREEN=$'\033[1;32m' diff --git a/compose.yaml b/compose.yaml index 993871b6..c39ec2b0 100644 --- a/compose.yaml +++ b/compose.yaml @@ -582,6 +582,11 @@ services: build: context: src/jmap-email command: pytest -q tests/ + environment: + # Hypothesis examples per `-m fuzz` test. The default matches + # FUZZ_SETTINGS in the suite; CI lowers it on pull requests so the + # job stays a few minutes and runs the full sweep on main. + FUZZ_EXAMPLES: ${FUZZ_EXAMPLES:-10000} volumes: - ./src/jmap-email/jmap_email:/app/jmap_email - ./src/jmap-email/tests:/app/tests diff --git a/deploy/python-uv/Dockerfile b/deploy/python-uv/Dockerfile index 5a3e4444..1f30f0be 100644 --- a/deploy/python-uv/Dockerfile +++ b/deploy/python-uv/Dockerfile @@ -33,8 +33,8 @@ WORKDIR /app FROM base AS uv # Pin uv by SHA256 digest for supply chain security. -# Verify with: gh attestation verify --owner astral-sh oci://ghcr.io/astral-sh/uv:0.11.28 -COPY --from=ghcr.io/astral-sh/uv@sha256:0f36cb9361a3346885ca3677e3767016687b5a170c1a6b88465ec14aefec90aa /uv /uvx /bin/ +# Verify with: gh attestation verify --owner astral-sh oci://ghcr.io/astral-sh/uv:0.12.13 +COPY --from=ghcr.io/astral-sh/uv@sha256:b485bd65cc2cf1c9a93b3554012c9c3778cf7b1b5fd3d3096ce9e1226c97e1e6 /uv /uvx /bin/ ENV UV_COMPILE_BYTECODE=1 ENV UV_LINK_MODE=copy @@ -45,7 +45,7 @@ ENV UV_PROJECT_ENVIRONMENT=/venv # Install Python via uv — integrity verified against SHA256 checksums embedded in # the uv binary. python-build-standalone statically links most C deps (ssl, ffi, # sqlite, zlib, lzma, bz2). -RUN uv python install 3.14.6 +RUN uv python install 3.14.7 # Prune list for the distroless images, shared by the backend and pymta builds # (they run `strip-python` in a stage off this image). Single source of truth. diff --git a/src/backend/Dockerfile b/src/backend/Dockerfile index 25a4868a..9ec7c797 100644 --- a/src/backend/Dockerfile +++ b/src/backend/Dockerfile @@ -1,5 +1,5 @@ # Shared base image (deploy/python-uv/Dockerfile): debian-trixie + apt upgrade + -# ca-certificates + uv 0.11.28 (digest-pinned) + uv-managed CPython 3.14.6 at +# ca-certificates + uv (digest-pinned) + uv-managed CPython (pinned) at # /opt/python. Global ARG so it can be used in `FROM` below. Built by # `make build-python-base`; overridden in CI. ARG PYTHON_UV_IMAGE=messages-python-uv:local diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index f5be6413..5a0369af 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -20,17 +20,17 @@ description = "A Django MDA" keywords = ["Django", "Contacts", "Templates", "RBAC"] license = "MIT" readme = "README.md" -requires-python = ">=3.14.6,<4.0" +requires-python = ">=3.14.7,<4.0" # Note: after changing this list you must re-run `make deps-lock-back` dependencies = [ "boto3==1.42.53", "botocore==1.42.53", "celery[redis]==5.6.2", - "cryptography==48.0.1", + "cryptography==50.0.1", "defusedxml==0.7.1", "dj-database-url==3.1.2", - "django==5.2.15", + "django==5.2.17", "django-celery-beat==2.8.1", "django-celery-results==2.6.0", "django-configurations==2.5.1", @@ -43,7 +43,7 @@ dependencies = [ "django-redis==6.0.0", "django-storages==1.14.6", "django-timezone-field==7.2.1", - "djangorestframework==3.16.1", + "djangorestframework==3.17.2", "dkimpy==1.1.8", "dnspython==2.8.0", "drf_spectacular==0.29.0", @@ -102,7 +102,7 @@ dev = [ ] [build-system] -requires = ["uv_build>=0.10.0,<0.11.0"] +requires = ["uv_build>=0.12.0,<0.13.0"] build-backend = "uv_build" [tool.uv.build-backend] @@ -111,7 +111,7 @@ source-include = ["core/**"] source-exclude = ["core/tests/**"] [tool.ruff] -# Pin ruff at py313 even though our runtime floor is py314.5 (chosen for +# Pin ruff at py313 even though our runtime floor is higher (chosen for # stdlib `email` fixes — see ``src/jmap-email/README.md``). Targeting py313 # keeps ruff's formatter from applying PEP 758 (un-parenthesized `except # E1, E2:`), which is valid Python 3.14 but trips up most static analysis diff --git a/src/backend/uv.lock b/src/backend/uv.lock index 7e494467..9078323a 100644 --- a/src/backend/uv.lock +++ b/src/backend/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.14.6, <4.0" +requires-python = ">=3.14.7, <4.0" [[package]] name = "aiofiles" @@ -247,14 +247,11 @@ wheels = [ [[package]] name = "click" -version = "8.3.1" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, ] [[package]] @@ -356,55 +353,52 @@ wheels = [ [[package]] name = "cryptography" -version = "48.0.1" +version = "50.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" }, - { url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" }, - { url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" }, - { url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" }, - { url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" }, - { url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" }, - { url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" }, - { url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" }, - { url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" }, - { url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" }, - { url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" }, - { url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" }, - { url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" }, - { url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" }, - { url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" }, - { url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" }, - { url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" }, - { url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" }, - { url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" }, - { url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" }, - { url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" }, - { url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" }, - { url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" }, - { url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" }, - { url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" }, - { url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" }, - { url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" }, - { url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" }, - { url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" }, - { url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" }, - { url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" }, - { url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" }, - { url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, ] [[package]] @@ -475,16 +469,16 @@ wheels = [ [[package]] name = "django" -version = "5.2.15" +version = "5.2.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/e3/31722f7284c9f43333daff9aee9184678e4487adcb5506af0db8cea09ce1/django-5.2.15.tar.gz", hash = "sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7", size = 10873669, upload-time = "2026-06-03T13:03:35.892Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/d8/43e9d000519adceb189620b6869ff88031e046df91c2e9da72f8f6918399/django-5.2.17.tar.gz", hash = "sha256:9d4d93be539a18ab80d058eb515900e10951e04c537c5a6b394fc49528d3251f", size = 10889740, upload-time = "2026-08-04T15:04:03.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/b5/38140b1643c00d5c46ce69c78e6980fd285aee223100319631bedee4f5e7/django-5.2.15-py3-none-any.whl", hash = "sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970", size = 8311957, upload-time = "2026-06-03T13:03:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/df/f8/ce120525ca78f12b07daf65786679c5d0b54a75285a8958d3ae55e39da35/django-5.2.17-py3-none-any.whl", hash = "sha256:f04fb3b36ee119e1af4fa1d397d5fd6cf12700f49321e84d4f4c642c5b1973db", size = 8315563, upload-time = "2026-08-04T15:03:59.1Z" }, ] [[package]] @@ -668,14 +662,14 @@ wheels = [ [[package]] name = "djangorestframework" -version = "3.16.1" +version = "3.17.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735, upload-time = "2025-08-06T17:50:53.251Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/35/c96055e700fdff25da3a7b7756cfd1d4dc54f38b9bc6d6c5e19e3a0fdc20/djangorestframework-3.17.2.tar.gz", hash = "sha256:89ed713b6dc83e1539f214b7d10808ae19bb8511004beba886225da6d5c9dafa", size = 906683, upload-time = "2026-08-05T07:47:22.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" }, + { url = "https://files.pythonhosted.org/packages/a2/46/c14108e400b208c394325eb63fbae06c81341b6447fa1a6f9da718b17fe7/djangorestframework-3.17.2-py3-none-any.whl", hash = "sha256:cb0546a7415d5b46c04e0f4fe0a54b2109f4fdd5e83ca773c8c6183a6493d042", size = 899109, upload-time = "2026-08-05T07:47:20.853Z" }, ] [[package]] @@ -828,24 +822,24 @@ wheels = [ [[package]] name = "h2" -version = "4.3.0" +version = "4.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "hpack" }, { name = "hyperframe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" }, ] [[package]] name = "hpack" -version = "4.1.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, ] [[package]] @@ -1242,10 +1236,10 @@ requires-dist = [ { name = "boto3", specifier = "==1.42.53" }, { name = "botocore", specifier = "==1.42.53" }, { name = "celery", extras = ["redis"], specifier = "==5.6.2" }, - { name = "cryptography", specifier = "==48.0.1" }, + { name = "cryptography", specifier = "==50.0.1" }, { name = "defusedxml", specifier = "==0.7.1" }, { name = "dj-database-url", specifier = "==3.1.2" }, - { name = "django", specifier = "==5.2.15" }, + { name = "django", specifier = "==5.2.17" }, { name = "django-celery-beat", specifier = "==2.8.1" }, { name = "django-celery-results", specifier = "==2.6.0" }, { name = "django-configurations", specifier = "==2.5.1" }, @@ -1259,7 +1253,7 @@ requires-dist = [ { name = "django-redis", specifier = "==6.0.0" }, { name = "django-storages", specifier = "==1.14.6" }, { name = "django-timezone-field", specifier = "==7.2.1" }, - { name = "djangorestframework", specifier = "==3.16.1" }, + { name = "djangorestframework", specifier = "==3.17.2" }, { name = "dkimpy", specifier = "==1.1.8" }, { name = "dnspython", specifier = "==2.8.0" }, { name = "drf-spectacular", specifier = "==0.29.0" }, @@ -1422,11 +1416,11 @@ wheels = [ [[package]] name = "pip" -version = "26.1.2" +version = "26.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/15/4500e320e6b101ec3b719ae85b697d9940b6cda672bc555bd6016fc60c6f/pip-26.2.1.tar.gz", hash = "sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f", size = 1848877, upload-time = "2026-08-04T22:51:14.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6e/1736e5b4ae2b778ef2f81c47d797de9f891d4d8acb047a24ca37a60294dd/pip-26.2.1-py3-none-any.whl", hash = "sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e", size = 1816632, upload-time = "2026-08-04T22:51:12.472Z" }, ] [[package]] @@ -1597,11 +1591,11 @@ wheels = [ [[package]] name = "pyasn1" -version = "0.6.3" +version = "0.6.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, ] [[package]] @@ -2156,11 +2150,11 @@ wheels = [ [[package]] name = "sqlparse" -version = "0.5.5" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/d3/3f06a1006f2261d1342aefb3c71eed02f5d4ca5bdbecd86ebc12ad38306e/sqlparse-0.6.0.tar.gz", hash = "sha256:113c35c75365ab9cc9c7231d68c6428fb11c085fc8e9eb1ad659b7ddbf6cd2b9", size = 178477, upload-time = "2026-08-13T19:16:06.396Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, + { url = "https://files.pythonhosted.org/packages/d9/50/f00935da0ec7cbf325f8dc4f772ae46fbc7b672dd62876e73f0a94adda57/sqlparse-0.6.0-py3-none-any.whl", hash = "sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f", size = 50070, upload-time = "2026-08-13T19:16:04.062Z" }, ] [[package]] @@ -2210,19 +2204,19 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.7" +version = "6.5.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/d3/343e5bb989d6515b1646cf3d40135d73f3d5e45339bded401b56cdac24dd/tornado-6.5.8.tar.gz", hash = "sha256:9452e1b208a8bd771e2cb1f2ff564985b9b214bdebbe622793e1799e0a6bd23f", size = 520493, upload-time = "2026-08-07T02:12:42.971Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, - { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, - { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, - { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, - { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, - { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, - { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d5/007086fd8df5489338e204f65adce33fd4f21a4999dbb2b9cff2f897b5f4/tornado-6.5.8-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:cc6aa787d7cfab7c3d35189dc7a56fbd2399a569624c730c6b55b3d6531d0403", size = 449487, upload-time = "2026-08-07T02:12:28.682Z" }, + { url = "https://files.pythonhosted.org/packages/70/c8/5a24a99495903f594f6a199dd7beead1cbc0a13e2cb9102727bcaaf2a997/tornado-6.5.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9715b5eb79735b2bcd454ce216a9275b7c0470e64ea1bf5742f78b2f72b26eeb", size = 447649, upload-time = "2026-08-07T02:12:30.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/de/f2e733f386b85962d1b1dc82cd63d169b5b4580062b35397eac9244a41fe/tornado-6.5.8-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:547d63f450d570c14fe0e8db2cfb14c9bbd1c2503b4a6612586267955aa47b58", size = 450707, upload-time = "2026-08-07T02:12:31.95Z" }, + { url = "https://files.pythonhosted.org/packages/0b/94/20efeee9a01c141e9ac47c397f81679dfda24b32768fc4fff24e76d36c2c/tornado-6.5.8-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2360a0ffbe145eca8af0b19cb7203d79b1a98dd4cccdd6b368f6f49c2e3808", size = 451677, upload-time = "2026-08-07T02:12:33.512Z" }, + { url = "https://files.pythonhosted.org/packages/42/ec/a96ccb8ccf0de2b7bc2c5fa1608a4803735018242e90c4882365a9fd418f/tornado-6.5.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5d242290bdf7ab3151bc1065fdd75c0dcc21cbc7b49f22a4c56329c2d6566d22", size = 451510, upload-time = "2026-08-07T02:12:35.346Z" }, + { url = "https://files.pythonhosted.org/packages/29/b5/93185859245ad3f00e62175f29607346788b696369347f0146e0421286bb/tornado-6.5.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7b94ff0e128fe0542f3bd331fb44d06260fc4ac16881545159f34ef08aad4195", size = 450917, upload-time = "2026-08-07T02:12:36.963Z" }, + { url = "https://files.pythonhosted.org/packages/97/cf/fe33cf062834487d34d1559746a4a12521033c22645b6d74d4bca702e018/tornado-6.5.8-cp39-abi3-win32.whl", hash = "sha256:67832909c4779c64942380cb5f044a5c6163d00831472d80e25e115de9917836", size = 451952, upload-time = "2026-08-07T02:12:38.512Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e1/468ad54333e92ccb62627e62cb88e5fc14a2171daa67ed47b1b8542d5b86/tornado-6.5.8-cp39-abi3-win_amd64.whl", hash = "sha256:11881db6b7c168494be2c2d12e65931451bdf7ee718535418ae1d8855dd5a0ee", size = 452391, upload-time = "2026-08-07T02:12:39.971Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3e/cd5e4f06e34cde33b8ef66cf36aa2b5ad46354cc1af7d2136bbe365fee1d/tornado-6.5.8-cp39-abi3-win_arm64.whl", hash = "sha256:68a7468c7e289f8514d7d664101753903217eff1bb6822c6b5994a0b5f5bcb26", size = 451411, upload-time = "2026-08-07T02:12:41.469Z" }, ] [[package]] diff --git a/src/jmap-email/CHANGELOG.md b/src/jmap-email/CHANGELOG.md index 3e30da46..2ca9594f 100644 --- a/src/jmap-email/CHANGELOG.md +++ b/src/jmap-email/CHANGELOG.md @@ -5,6 +5,42 @@ All notable changes to `jmap-email` are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.1] - 2026-09-11 + +### Fixed + +- A sender-chosen charset can no longer sink a message. `bytes.decode` + rejects some charsets with an exception that is not + `UnicodeDecodeError`, and the two decode sites only caught that one: + `idna` and `undefined` are registered codecs that refuse the call + itself (bare `UnicodeError`), and an embedded NUL raises `ValueError`. + A single `Subject: =?idna?B?...?=` header made `parse_email` return + `None` — the whole message lost — and the same charset on a body part + dropped every body part with a `BodyStructureWalkError` defect. Both + sites now fall back to UTF-8 with replacement, as they already did for + an unknown charset. + +- An out-of-range `Date:` can no longer sink a message on Python 3.14.6. + `parsedate_to_datetime` raised `OverflowError` for a numeric zone or + year too large for the C int behind `timedelta`/`datetime`, and neither + `parse_date` nor the composer's date coercion caught it — a header such + as `Date: Mon, 1 Jan 2024 00:00:00 +99999999999999999999` unwound into + `parse_email`'s catch-all and returned `None`. CPython 3.14.7 turned it + into a `ValueError` + ([gh-153406](https://github.com/python/cpython/issues/153406)); both + are now caught, so the fix applies across the supported range rather + than by raising the floor. + +### Testing + +- Fuzz strategies now cover date-shaped input and hostile charset names. + The previous strategies were generic evil text, which never survives + `parsedate_tz` (0 of 20000 samples produced a date tuple) and never + forms a well-formed encoded-word, so neither branch above was reachable. +- Fuzz tests now fail when the parser's catch-all handlers fire. Every + oracle accepts `None` and a body-less message, so an internal exception + used to read as a pass. + ## [0.3.0] - 2026-08-05 ### Changed @@ -196,6 +232,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial release. Extracted from the [Messages](https://github.com/suitenumerique/messages) project. +[0.3.1]: https://github.com/suitenumerique/messages/releases/tag/jmap-email-0.3.1 [0.3.0]: https://github.com/suitenumerique/messages/releases/tag/jmap-email-0.3.0 [0.2.0]: https://github.com/suitenumerique/messages/releases/tag/jmap-email-0.2.0 [0.1.0]: https://github.com/suitenumerique/messages/releases/tag/jmap-email-0.1.0 diff --git a/src/jmap-email/Dockerfile b/src/jmap-email/Dockerfile index 02a7f3b9..f3b48f4b 100644 --- a/src/jmap-email/Dockerfile +++ b/src/jmap-email/Dockerfile @@ -1,6 +1,6 @@ # Test image for the jmap-email package. Inherits the shared python-uv base -# (managed CPython 3.14.6 + uv). NOTE: the PyPI *release* still builds on the -# official python:3.14.6-slim image via bin/release-jmap-email.sh — that is the +# (managed CPython + uv). NOTE: the PyPI *release* still builds on the +# official python slim image via bin/release-jmap-email.sh — that is the # release-parity environment; this image is only for CI lint/type/test. # One runtime dep (idna); plus pytest + hypothesis for the suite, and # ``ty``/``ruff``/``pylint`` for type-checking and linting. diff --git a/src/jmap-email/README.md b/src/jmap-email/README.md index 77782f5f..7d298513 100644 --- a/src/jmap-email/README.md +++ b/src/jmap-email/README.md @@ -31,6 +31,13 @@ it carries policies), which materially affects the composer, plus the further `email` fixes shipped in 3.14.6. +Where a later fix is one we can absorb ourselves, we do that instead of +raising the floor — 3.14.7's +[gh-153406](https://github.com/python/cpython/issues/153406) +(`parsedate_to_datetime` raising `ValueError` rather than +`OverflowError` on an out-of-range `Date:`) is handled by catching both, +so a 3.14.6 deployment is not left behind by a bug-fix release. + **Aligning on the latest 3.14.x patch is recommended for any production deployment.** Each CPython patch release that touches `email` is one less class of malformed-input edge case downstream diff --git a/src/jmap-email/jmap_email/__init__.py b/src/jmap-email/jmap_email/__init__.py index ab91085f..c6ff5dc4 100644 --- a/src/jmap-email/jmap_email/__init__.py +++ b/src/jmap-email/jmap_email/__init__.py @@ -16,7 +16,7 @@ Versioning: semantic. Public API is everything exported below; anything prefixed with ``_`` is internal. """ -__version__ = "0.3.0" +__version__ = "0.3.1" # The RFC 8621 ``TypedDict`` shapes are annotation-only and live in their # own namespace — ``from jmap_email.types import JmapEmail`` — rather than diff --git a/src/jmap-email/jmap_email/composer.py b/src/jmap-email/jmap_email/composer.py index 0162b255..1baf0803 100644 --- a/src/jmap-email/jmap_email/composer.py +++ b/src/jmap-email/jmap_email/composer.py @@ -519,7 +519,9 @@ def _normalize_date(date) -> datetime.datetime: pass try: return _attach_utc_if_naive(parsedate_to_datetime(date)) - except (ValueError, TypeError, IndexError): + except (ValueError, TypeError, IndexError, OverflowError): + # ``OverflowError``: see ``parse_date`` — below CPython 3.14.7 an + # out-of-range zone or year raises it instead of ``ValueError``. pass raise InvalidDateError( f"'sentAt' string is neither ISO-8601 nor RFC 2822: {date!r}" diff --git a/src/jmap-email/jmap_email/parser.py b/src/jmap-email/jmap_email/parser.py index 284ad4c1..620ebf78 100644 --- a/src/jmap-email/jmap_email/parser.py +++ b/src/jmap-email/jmap_email/parser.py @@ -279,7 +279,16 @@ def decode_rfc2047_header(header_text: str) -> str: else: try: result_parts.append(part.decode(charset, errors="replace")) - except (LookupError, UnicodeDecodeError): + except (LookupError, ValueError, TypeError): + # The charset is sender-controlled, so the catch covers + # every way ``bytes.decode`` can reject it, not just a + # bad-bytes ``UnicodeDecodeError``: ``LookupError`` for an + # unknown or non-text codec, ``ValueError`` for an + # embedded NUL and for the codecs that refuse the call + # itself (``idna`` rejects ``errors="replace"``, + # ``undefined`` rejects everything — both raise a bare + # ``UnicodeError``), ``TypeError`` for a non-``str`` + # charset out of the header parser. result_parts.append(part.decode("utf-8", errors="replace")) else: # Part is already a string. Repair surrogate-escaped 8-bit @@ -821,7 +830,11 @@ def parse_date(date_str: str) -> datetime | None: try: # Use email.utils which handles RFC 5322 date formats return parsedate_to_datetime(date_str) - except (TypeError, ValueError) as e: # Catch specific errors + except (TypeError, ValueError, OverflowError) as e: + # ``OverflowError`` for a numeric zone or year too large for the C + # int behind ``timedelta``/``datetime``. CPython 3.14.7 turned that + # into a ``ValueError`` (gh-153406), but the floor is 3.14.6 and a + # ``Date:`` header is sender-controlled, so it stays caught here. logger.warning("Could not parse date string '%s': %s", date_str, e) return None @@ -1372,7 +1385,10 @@ def _build_body_part_dict(part_info: dict[str, Any]) -> tuple[EmailBodyPart, boo charset = part_info.get("charset") or "utf-8" try: content = body.decode(charset, errors="replace") - except (LookupError, UnicodeDecodeError): + except (LookupError, ValueError, TypeError): + # See ``decode_rfc2047_header`` for why the catch is this wide: + # the charset comes off the wire, and a decode call can be + # rejected for reasons that are not ``UnicodeDecodeError``. content = body.decode("utf-8", errors="replace") encoding_problem = True size = len(body) diff --git a/src/jmap-email/pyproject.toml b/src/jmap-email/pyproject.toml index e8aaa7f3..af66c04e 100644 --- a/src/jmap-email/pyproject.toml +++ b/src/jmap-email/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jmap-email" -version = "0.3.0" +version = "0.3.1" description = "A strict-JMAP RFC 8621 Email object library for Python 3.14+ with lenient RFC 5322 / MIME parsing and strict-by-design composition. One runtime dependency: idna (UTS 46 domain encoding)." readme = "README.md" license = "MIT" @@ -88,7 +88,7 @@ markers = [ ] [tool.ruff] -# Pin py313 even though the runtime floor is py314.5. Targeting py313 +# Pin py313 even though the runtime floor is higher. Targeting py313 # keeps ruff's formatter from applying PEP 758 (un-parenthesized # ``except E1, E2:``) — valid Python 3.14 but trips static analysis # (CodeRabbit, IDEs on older Python, copy-pasted snippets). diff --git a/src/jmap-email/tests/conftest.py b/src/jmap-email/tests/conftest.py new file mode 100644 index 00000000..20dc2cae --- /dev/null +++ b/src/jmap-email/tests/conftest.py @@ -0,0 +1,62 @@ +"""Shared test fixtures. + +The parser has two "this should never happen" handlers that turn an +internal exception into a degraded result: ``parse_email`` returns +``None``, and the body-structure walk returns a message with no body +parts. Every fuzz oracle accepts both of those outcomes (``assert result +is None or isinstance(result, dict)``, ``if parsed is None: return``), so +on their own the fuzz tests cannot see the one thing they exist to find. + +``fail_on_swallowed_exception`` closes that gap for fuzz-marked tests: if +either handler fires, the test fails with the swallowed exception instead +of passing on a degraded result. +""" + +import logging + +import pytest + +# Log messages emitted by the parser's two catch-all handlers. +_SWALLOW_MARKERS = ( + "parse_email: unexpected error", + "Error parsing message body structure", +) + + +class _SwallowDetector(logging.Handler): + """Records the parser's catch-all handler firing.""" + + def __init__(self): + super().__init__(level=logging.WARNING) + self.hits: list[str] = [] + + def emit(self, record: logging.LogRecord) -> None: + message = record.getMessage() + if any(marker in message for marker in _SWALLOW_MARKERS): + exc = record.exc_info[1] if record.exc_info else None + self.hits.append(f"{message} [{exc!r}]") + + +@pytest.fixture(autouse=True) +def fail_on_swallowed_exception(request): + """Fail a fuzz test whose input made the parser swallow an exception.""" + if "fuzz" not in request.keywords: + yield + return + + logger = logging.getLogger("jmap_email") + detector = _SwallowDetector() + was_disabled = logger.disabled + logger.disabled = False + logger.addHandler(detector) + try: + yield + finally: + logger.removeHandler(detector) + logger.disabled = was_disabled + + if detector.hits: + raise AssertionError( + "parser swallowed an exception on fuzz input " + f"({len(detector.hits)} time(s)); first: {detector.hits[0]}" + ) diff --git a/src/jmap-email/tests/test_address_fuzz.py b/src/jmap-email/tests/test_address_fuzz.py index 463b903e..ecd09762 100644 --- a/src/jmap-email/tests/test_address_fuzz.py +++ b/src/jmap-email/tests/test_address_fuzz.py @@ -131,12 +131,81 @@ evil_text = st.one_of( ), # Control chars ) +# Date-shaped evil inputs. ``evil_text`` alone never reaches the interesting +# half of ``parse_date``: unstructured text does not survive ``parsedate_tz``, +# so it only ever exercises the "unparseable -> None" branch. These keep the +# RFC 5322 shape and attack the numeric fields instead, which is where the +# stdlib builds a ``datetime``/``timedelta`` and can overflow a C int. +evil_number = st.one_of( + st.integers(min_value=-100, max_value=100), + st.integers(min_value=0, max_value=10**30), + st.integers(min_value=-(10**30), max_value=0), + st.sampled_from([2**31 - 1, 2**31, 2**63, 9999999999, 0, -1]), +) + +evil_date = st.one_of( + st.builds( + lambda day, year, hh, mm, ss, sign, zone: ( + f"Mon, {day} Jan {year} {hh}:{mm}:{ss} {sign}{zone}" + ), + evil_number, + evil_number, + evil_number, + evil_number, + evil_number, + st.sampled_from(["+", "-", ""]), + evil_number.map(abs), + ), + # Named zones and the obsolete single-letter military ones. + st.builds( + lambda day, year, zone: f"{day} Jan {year} 00:00:00 {zone}", + evil_number, + evil_number, + st.sampled_from(["UT", "GMT", "EST", "Z", "A", "J", "XYZZY", ""]), + ), + # Structure kept, separators mangled. + st.builds( + lambda a, b: f"Mon, 1 Jan 2024 00:00:00 +{a}{b}", + evil_number.map(abs), + st.text(max_size=10), + ), +) + + +# Charset tokens for encoded-words. Random text only ever reaches the +# LookupError path; these are *registered* stdlib codecs that reject the +# ``bytes.decode(name, errors="replace")`` call itself (``idna`` on the +# error handler, ``undefined`` on everything, the byte-to-byte codecs +# because they are not text encodings). The charset is sender-controlled. +evil_charset = st.one_of( + st.text(max_size=20), + st.sampled_from( + [ + "utf-8", + "iso-8859-1", + "idna", + "undefined", + "punycode", + "unicode_escape", + "raw_unicode_escape", + "hex_codec", + "base64_codec", + "quopri_codec", + "uu_codec", + "zlib_codec", + "bz2_codec", + "rot_13", + ] + ), +) + + # Header-specific evil inputs - things that commonly appear in email headers header_evil = st.one_of( # RFC 2047 encoded words - malformed variants st.builds( lambda charset, encoding, text: f"=?{charset}?{encoding}?{text}?=", - st.text(max_size=20), + evil_charset, st.sampled_from(["Q", "B", "q", "b", "", "X", "QQ", "?", "\x00"]), st.text(max_size=100), ), @@ -269,6 +338,15 @@ class TestAddressParserFuzzing: result = decode_rfc2047_header(text) assert isinstance(result, str) + @given(text=header_evil) + @settings(**FUZZ_SETTINGS) + def test_decode_email_header_shaped_input_never_crashes(self, text): + """Same contract on header-shaped input. ``evil_text`` never forms a + well-formed encoded-word, so it never reaches the charset lookup — + which is where a sender-chosen codec can reject the decode call.""" + result = decode_rfc2047_header(text) + assert isinstance(result, str) + @given(date_str=evil_text) @settings(**FUZZ_SETTINGS) def test_parse_date_never_crashes(self, date_str): @@ -276,6 +354,15 @@ class TestAddressParserFuzzing: result = parse_date(date_str) assert result is None or hasattr(result, "year") + @given(date_str=evil_date) + @settings(**FUZZ_SETTINGS) + def test_parse_date_shaped_input_never_crashes(self, date_str): + """Same contract, but on input that actually reaches the stdlib's + ``datetime`` construction — out-of-range years and numeric zones + included. See ``evil_date`` for why ``evil_text`` cannot.""" + result = parse_date(date_str) + assert result is None or hasattr(result, "year") + @pytest.mark.fuzz class TestAddressEdgeCasesFuzzing: diff --git a/src/jmap-email/tests/test_ambiguity_defects.py b/src/jmap-email/tests/test_ambiguity_defects.py index c0451696..6b29d7a0 100644 --- a/src/jmap-email/tests/test_ambiguity_defects.py +++ b/src/jmap-email/tests/test_ambiguity_defects.py @@ -436,7 +436,7 @@ class TestOpaqueAndAmbiguousPartMarkers: class TestStdlibEmailCveRegressions: - """Behaviour we inherit from CPython, pinned because the 3.14.6 floor + """Behaviour we inherit from CPython, pinned because the version floor exists precisely to carry ``email`` fixes — a downgrade or a vendored stdlib would reintroduce these silently.""" diff --git a/src/jmap-email/tests/test_message_fuzz.py b/src/jmap-email/tests/test_message_fuzz.py index bbd3fc50..1bb5c9f7 100644 --- a/src/jmap-email/tests/test_message_fuzz.py +++ b/src/jmap-email/tests/test_message_fuzz.py @@ -226,6 +226,23 @@ charsets = st.sampled_from( "utf_8", "UTF8", "UNKNOWN", + # Registered stdlib codecs that reject the *call* rather than the + # bytes: ``idna`` refuses ``errors="replace"``, ``undefined`` refuses + # everything, and the byte-to-byte codecs are not text encodings. + # A charset is sender-controlled, so all of them are reachable; the + # hand-written names above only ever exercised the LookupError path. + "idna", + "undefined", + "punycode", + "unicode_escape", + "raw_unicode_escape", + "hex_codec", + "base64_codec", + "quopri_codec", + "uu_codec", + "zlib_codec", + "bz2_codec", + "rot_13", ] ) diff --git a/src/jmap-email/tests/test_parser.py b/src/jmap-email/tests/test_parser.py index acfb3b8c..c5e61a6d 100644 --- a/src/jmap-email/tests/test_parser.py +++ b/src/jmap-email/tests/test_parser.py @@ -14,6 +14,7 @@ from email.header import Header import pytest from jmap_email import DEFAULT_PARSE_OPTIONS +from jmap_email import parser as parser_module from jmap_email.parser import ( _parse_message_content, decode_rfc2047_header, @@ -736,6 +737,89 @@ class TestDateParsing: assert parsed.month == 9 assert parsed.day == 15 + @pytest.mark.parametrize( + "date_str", + [ + "Mon, 1 Jan 2024 00:00:00 +99999999999999999999", + "Mon, 1 Jan 9999999999 00:00:00 +0000", + ], + ) + def test_parse_date_out_of_range_returns_none(self, date_str): + """A numeric zone or year too large for the C int behind + ``timedelta``/``datetime`` must degrade to ``None``, not escape. + + CPython 3.14.7 raises ``ValueError`` here; 3.14.6 raised + ``OverflowError`` (gh-153406). The supported range spans both, so + the assertion has to hold either way — see + ``test_parse_date_catches_overflow_error`` for the 3.14.6 shape + pinned independently of the running interpreter. + """ + assert parse_date(date_str) is None + + def test_parse_date_catches_overflow_error(self, monkeypatch): + """``OverflowError`` stays caught even on an interpreter that no + longer raises it, so the 3.14.6 floor keeps working. + + Pinned by injection rather than by a crafted date: on 3.14.7 no + input reaches this branch, and without the injection the test + would silently stop testing anything. + """ + + def _raise(*_args, **_kwargs): + raise OverflowError("Python int too large to convert to C int") + + monkeypatch.setattr(parser_module, "parsedate_to_datetime", _raise) + assert parse_date("Mon, 1 Jan 2024 00:00:00 +0000") is None + + def test_hostile_charset_in_encoded_word_does_not_lose_the_message(self): + """A sender-chosen charset can make ``bytes.decode`` raise something + other than ``UnicodeDecodeError``: ``idna`` and ``undefined`` are + registered codecs that reject the call itself, and an embedded NUL + or a lone surrogate raises ``ValueError``. None of it may sink the + message — a one-line ``Subject:`` is the whole attack. + """ + for charset in ("idna", "undefined", "\x00", "utf-8\x00", "\ud800"): + encoded = f"=?{charset}?B?gIGCgw==?=" + assert isinstance(decode_rfc2047_header(encoded), str) + + # ``errors="replace"``: a lone surrogate reaches the public API + # as ``str`` but has no UTF-8 wire form, so it cannot be encoded + # verbatim into the raw message. + raw = f"From: a@b.com\r\nSubject: {encoded}\r\n\r\nbody\r\n".encode( + "utf-8", errors="replace" + ) + parsed = parse_email(raw) + assert parsed is not None, f"message lost for charset={charset}" + + def test_hostile_charset_on_body_keeps_the_body(self): + """Same codecs on a body part. The body must survive via the UTF-8 + fallback rather than being dropped by the body-structure walk.""" + for charset in ("idna", "undefined", "bz2_codec"): + raw = ( + b"From: a@b.com\r\nSubject: hi\r\nMIME-Version: 1.0\r\n" + b'Content-Type: text/plain; charset="' + charset.encode() + b'"\r\n' + b"\r\nsecret body text\r\n" + ) + parsed = parse_email(raw) + assert parsed is not None + assert parsed["textBody"], f"body dropped for charset={charset}" + assert "secret body text" in parsed["preview"] + + def test_out_of_range_date_does_not_lose_the_message(self): + """The rest of the message must survive an unparseable ``Date:``.""" + raw = ( + b"From: sender@example.com\r\n" + b"To: rcpt@example.com\r\n" + b"Subject: still here\r\n" + b"Date: Mon, 1 Jan 2024 00:00:00 +99999999999999999999\r\n" + b"\r\n" + b"body\r\n" + ) + parsed = parse_email(raw) + assert parsed is not None + assert parsed["sentAt"] is None + assert parsed["subject"] == "still here" + @pytest.mark.django_db class TestEmailMessageParsing: diff --git a/src/jmap-email/uv.lock b/src/jmap-email/uv.lock index 942672db..2cf2ef78 100644 --- a/src/jmap-email/uv.lock +++ b/src/jmap-email/uv.lock @@ -132,7 +132,7 @@ wheels = [ [[package]] name = "jmap-email" -version = "0.3.0" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "idna" }, diff --git a/src/mpa/tests/Dockerfile b/src/mpa/tests/Dockerfile index f476c417..9a04cb44 100644 --- a/src/mpa/tests/Dockerfile +++ b/src/mpa/tests/Dockerfile @@ -1,5 +1,5 @@ # Test harness for the rspamd MPA. Inherits the shared python-uv base -# (managed CPython 3.14.6 + uv); built by `make build-python-base`. +# (managed CPython + uv); built by `make build-python-base`. ARG PYTHON_UV_IMAGE=messages-python-uv:local FROM ${PYTHON_UV_IMAGE} diff --git a/src/mta-in/Dockerfile b/src/mta-in/Dockerfile index e549f10b..2a7d6801 100644 --- a/src/mta-in/Dockerfile +++ b/src/mta-in/Dockerfile @@ -2,7 +2,7 @@ # (pure-Python aiosmtpd). This is the production default for now. # # Python comes from the shared uv-managed base (deploy/python-uv/Dockerfile: -# debian-trixie + uv 0.11.28 digest-pinned + CPython 3.14.6), NOT the official +# debian-trixie + uv digest-pinned + CPython pinned), NOT the official # python image — so the interpreter matches the backend and pymta images. # # Global ARG so PYTHON_UV_IMAGE can be used in `FROM`. Built by diff --git a/src/mta-in/pyproject.toml b/src/mta-in/pyproject.toml index 1d53cb74..f6ef3762 100644 --- a/src/mta-in/pyproject.toml +++ b/src/mta-in/pyproject.toml @@ -2,7 +2,7 @@ # st-messages-mta-in package # [build-system] -requires = ["uv_build>=0.11.0,<0.12.0"] +requires = ["uv_build>=0.12.0,<0.13.0"] build-backend = "uv_build" [project] @@ -23,7 +23,7 @@ description = "A stateless MTA." keywords = ["Python", "MTA"] license = "MIT" readme = "README.md" -requires-python = ">=3.14.6,<4.0" +requires-python = ">=3.14.7,<4.0" # Note: after changing this list you must re-run `make deps-lock-mta-in` dependencies = [ diff --git a/src/mta-in/uv.lock b/src/mta-in/uv.lock index 444f1dd5..d53b5189 100644 --- a/src/mta-in/uv.lock +++ b/src/mta-in/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.14.6, <4.0" +requires-python = ">=3.14.7, <4.0" [[package]] name = "aiosmtpd" diff --git a/src/socks-proxy/tests/Dockerfile b/src/socks-proxy/tests/Dockerfile index e31596b2..59e895b2 100644 --- a/src/socks-proxy/tests/Dockerfile +++ b/src/socks-proxy/tests/Dockerfile @@ -1,5 +1,5 @@ # Test harness for the socks-proxy (dante). Inherits the shared python-uv base -# (managed CPython 3.14.6 + uv); built by `make build-python-base`. +# (managed CPython + uv); built by `make build-python-base`. ARG PYTHON_UV_IMAGE=messages-python-uv:local FROM ${PYTHON_UV_IMAGE}