mirror of
https://github.com/suitenumerique/messages.git
synced 2026-08-17 21:25:41 +02:00
🔖(jmap_email) release 0.3.0
Bump to version 0.3.0
This commit is contained in:
@@ -139,7 +139,9 @@ with zipfile.ZipFile(WHEEL) as zf:
|
||||
|
||||
expected_wheel = {
|
||||
"jmap_email/__init__.py",
|
||||
"jmap_email/addresses.py",
|
||||
"jmap_email/composer.py",
|
||||
"jmap_email/filenames.py",
|
||||
"jmap_email/helpers.py",
|
||||
"jmap_email/options.py",
|
||||
"jmap_email/parser.py",
|
||||
@@ -293,9 +295,10 @@ echo " https://test.pypi.org/project/jmap-email/${VERSION}/"
|
||||
|
||||
# ── 5. smoke install ──────────────────────────────────────────────────────
|
||||
say "→ Smoke-installing jmap-email==${VERSION} from TestPyPI"
|
||||
# TestPyPI's index has limited transitive coverage; jmap-email has zero
|
||||
# runtime deps so a bare TestPyPI install is fine. The retry loop covers
|
||||
# the index-propagation lag (~30s after upload).
|
||||
# TestPyPI's index has limited transitive coverage, so the runtime dep
|
||||
# (idna) is resolved from real PyPI via --extra-index-url; only the
|
||||
# package under test comes from TestPyPI. The retry loop covers the
|
||||
# index-propagation lag (~30s after upload).
|
||||
docker run --rm -t \
|
||||
"${PYTHON_IMAGE}" \
|
||||
bash -c "
|
||||
@@ -303,6 +306,7 @@ docker run --rm -t \
|
||||
for i in 1 2 3 4 5; do
|
||||
if pip install --quiet --no-cache-dir \
|
||||
--index-url https://test.pypi.org/simple/ \
|
||||
--extra-index-url https://pypi.org/simple/ \
|
||||
jmap-email==${VERSION}; then
|
||||
break
|
||||
fi
|
||||
|
||||
+131
-1
@@ -5,7 +5,136 @@ 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).
|
||||
|
||||
## [Unreleased]
|
||||
## [0.3.0] - 2026-08-05
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking:** the package is no longer dependency-free: IDNA encoding
|
||||
(`ComposeOptions(idna_encode_domains=True)`) now uses the
|
||||
[`idna`](https://pypi.org/project/idna/) package (UTS 46,
|
||||
non-transitional) instead of the stdlib IDNA2003 codec. The accepted
|
||||
range is `idna>=3.7,<4` — the floor is the CVE-2024-3651 fix, the cap
|
||||
keeps a future UTS 46 revision from changing what a domain encodes to
|
||||
without a release here. Deviation code points are no longer
|
||||
folded — `faß.de` encodes to `xn--fa-hia.de` instead of silently
|
||||
becoming the *distinct* registrable domain `fass.de` (likewise the
|
||||
Greek final sigma) — and labels IDNA2008 disallows (emoji) are now
|
||||
refused. Empty labels, over-long labels, and a trailing root dot are
|
||||
refused as before.
|
||||
|
||||
- **Breaking:** `parse_email` returns `None` when any header field
|
||||
exceeds `max_header_value_bytes` (previously the value was truncated
|
||||
and the message parsed).
|
||||
- `decode_rfc2047_header` bounds its own input at
|
||||
`max_header_value_bytes`: the stdlib's `decode_header` is O(n²) in the
|
||||
number of encoded-words, and attachment filenames reach it untruncated.
|
||||
- The unexported `MAX_*` mirror constants on `jmap_email.parser` are
|
||||
removed; read the fields on `DEFAULT_PARSE_OPTIONS` instead.
|
||||
- **Breaking:** `compose_email`'s `keep_bcc` argument moved into a
|
||||
`ComposeOptions` bundle as `emit_bcc`, for symmetry with `ParseOptions`:
|
||||
`compose_email(jmap, options=ComposeOptions(emit_bcc=True))`.
|
||||
`in_reply_to` and `prepend_headers` stay keyword arguments.
|
||||
- **Breaking:** `compose_email`'s `allow_extensions` argument is removed;
|
||||
`_ext` is always accepted. The composer never read it, so the flag could
|
||||
reject input but never change output.
|
||||
- `msgid_chain` drops entries it cannot write into a header (line
|
||||
terminator, internal whitespace, nested angle bracket) instead of
|
||||
emitting them. Well-formed ids, including `<foo$@local@domain>` and
|
||||
`<12345>`, are unaffected.
|
||||
- `sent_at_to_datetime` always returns a tz-aware datetime; naive input is
|
||||
stamped UTC. A naive return made comparison with an aware datetime raise.
|
||||
- `preview_text` no longer lets a line-anchored markdown construct span
|
||||
lines: `"alpha\n-\n-\n-\nbravo"` was previewing as `alpha bravo`,
|
||||
now `alpha - - - bravo`.
|
||||
- The shape accessors accept only a `list`; a tuple or generator now
|
||||
returns the empty default.
|
||||
|
||||
### Added
|
||||
|
||||
- `ComposeOptions` / `DEFAULT_COMPOSE_OPTIONS`, the compose-side peer of
|
||||
`ParseOptions`: frozen, hashable, `dataclasses.replace`-able.
|
||||
- `idna_encode_domains` (default `False`) — IDNA-encodes a non-ASCII
|
||||
**domain** to its A-label (`contact@exemplé.fr` →
|
||||
`contact@xn--exempl-gva.fr`), the only form 7-bit SMTP and the MX
|
||||
lookup carry. Nothing in the stdlib does this conversion.
|
||||
- `allow_8bit` (default `False`) — emits non-ASCII bodies as 8-bit.
|
||||
Needs 8BITMIME (RFC 6152) on the hop.
|
||||
- `allow_smtputf8` (default `False`) — emits UTF-8 headers (RFC 6532)
|
||||
and permits a non-ASCII local part. Needs SMTPUTF8 (RFC 6531) on the
|
||||
hop. Implies `allow_8bit`.
|
||||
|
||||
The `allow_*` pair names hop capabilities this library does not
|
||||
discover; the defaults assume nothing and emit pure-ASCII 7-bit. They
|
||||
are permissions, not instructions — an all-ASCII message composes
|
||||
byte-identically either way. See the README for the
|
||||
compose-both-and-fall-back pattern.
|
||||
- `sanitize_filename` is now public, for names that never went through
|
||||
`parse_email`. Returns `None`, never `""`. A nameless part still reports
|
||||
`name: null`; synthesizing a placeholder stays consumer policy.
|
||||
- `is_valid_addr_spec` is now public and shared by the parser and the
|
||||
composer: `True` means one well-formed mailbox, usable as it stands.
|
||||
- Sixteen `_ext.defects` markers for MIME constructs different parsers
|
||||
resolve differently, from the *Inbox Invasion* (CCS '24) and *Email
|
||||
Smuggling* (2025) evasion catalogues. See the README table.
|
||||
`message/rfc822` is deliberately unmarked — it fires on every forward.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Remote DoS: deeply nested MIME.** `BufferedSubFile.readline` tests
|
||||
every body line against every ancestor predicate, making the stdlib
|
||||
parse O(depth × lines). `_FastSubFile` skips that scan for lines that
|
||||
cannot be a delimiter: cost flat in depth, identical output.
|
||||
- **Sender forgery via header truncation.** Header values were cut at a
|
||||
byte bound and *then* parsed, so a padded `From` could be made to
|
||||
parse to an address nobody sent — which became the stored sender and
|
||||
the DKIM alignment domain. Over-long fields are now rejected; address
|
||||
lists cut back to a top-level separator and record
|
||||
`AddressListTruncatedDefect`.
|
||||
- **A display name could become a recipient**, two ways: the quoting check
|
||||
treated a lone `"` as an already-quoted name, and RFC 2047 decoding
|
||||
happened after the quoting decision, so `=?utf-8?B?ZXZpbEB4LmNv?=`
|
||||
went out unquoted and decoded to a second mailbox.
|
||||
- **An unclosed comment let a display name become the parsed sender.**
|
||||
`From: victim@bank.com( <attacker@evil.co>` reported `victim@bank.com`.
|
||||
CVE-2023-27043 by another route; such headers are now refused.
|
||||
- **Addr-spec validation was too loose.** A comma made one value two
|
||||
recipients; RFC 5322 specials (`( ) [ ] : \`) unquoted changed the
|
||||
recipient count or mutated the address; an unterminated quoted-string
|
||||
local-part swallowed the next recipient; a control character was
|
||||
silently cleaned and a different address emitted.
|
||||
- **Non-ASCII addr-specs were silently mangled** into encoded-words that
|
||||
RFC 2047 §5 forbids. Now converted under `idna_encode_domains` /
|
||||
`allow_smtputf8`, or refused.
|
||||
- **MIME boundaries now come from a CSPRNG**, not Mersenne Twister.
|
||||
- **Quadratic matching reachable from attacker input.** The angle-addr
|
||||
check ran `<[^<>]*@[^<>]*>`, where both halves can match `@`, so a `<`
|
||||
followed by a run of `@` made the engine try every split point — one
|
||||
96 KiB message cost ~43s of CPU in `parse_email`. The markdown link and
|
||||
autolink patterns had the same shape: at `max_chars=65536` a body of
|
||||
repeated `[` or `<` cost 65s and 114s in `preview_text`. All are now
|
||||
anchored or bounded, and linear.
|
||||
- **Malformed shapes raised `AttributeError` before being wrapped.**
|
||||
`format_address_list` and the attachment partition called `.get` on
|
||||
entries that need not be dicts, so `{"to": "x"}` logged a full traceback
|
||||
on every call — a log-flooding vector. Address entries that are not
|
||||
dicts are skipped; a non-dict attachment raises `AttachmentError`,
|
||||
since dropping one is invisible data loss.
|
||||
- **The null-safe accessors could raise** on a truthy non-iterable
|
||||
`headers` or a non-string header name.
|
||||
- `parse_email` threads `options=` into address parsing, so
|
||||
`max_address_list_bytes` applies on the entry point that meets hostile
|
||||
mail; and defects are harvested after body parsing, so decode-time
|
||||
defects are no longer dropped.
|
||||
- RFC 6266 §4.3: `filename*` now wins over `filename`.
|
||||
|
||||
### Internal
|
||||
|
||||
- Four new Hypothesis suites under `pytest -m fuzz` (parse/compose seam,
|
||||
wire round-trip, filenames, helpers) plus `test_ambiguity_defects`.
|
||||
Fuzz phases are Hypothesis's defaults now, so failures shrink and
|
||||
replay instead of vanishing.
|
||||
- `preview_text` gained wall-clock complexity guards: the line-anchored
|
||||
patterns were quadratic in the head, whose size scales with `max_chars`.
|
||||
|
||||
## [0.2.0] - 2026-07-22
|
||||
|
||||
@@ -67,5 +196,6 @@ 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.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
|
||||
|
||||
@@ -16,7 +16,7 @@ Versioning: semantic. Public API is everything exported below; anything
|
||||
prefixed with ``_`` is internal.
|
||||
"""
|
||||
|
||||
__version__ = "0.2.0"
|
||||
__version__ = "0.3.0"
|
||||
|
||||
# The RFC 8621 ``TypedDict`` shapes are annotation-only and live in their
|
||||
# own namespace — ``from jmap_email.types import JmapEmail`` — rather than
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "jmap-email"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
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"
|
||||
|
||||
Generated
+14
-1
@@ -103,6 +103,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/0f/b61fd8f8f98c8cc6c99f3df670b379e907ef842d3290a9c86ad1441e627e/hypothesis-6.159.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d966f544231ed420e6b8d59ba97539d767bc848c4de311f213023050e9b6d2da", size = 654269, upload-time = "2026-07-22T08:59:55.251Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.18"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
@@ -123,8 +132,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "jmap-email"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
@@ -139,6 +151,7 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.151.0" },
|
||||
{ name = "idna", specifier = ">=3.7,<4" },
|
||||
{ name = "pylint", marker = "extra == 'dev'", specifier = ">=4.0.4" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.0" },
|
||||
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" },
|
||||
|
||||
Reference in New Issue
Block a user