fast-mail-parser-ng 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. fast_mail_parser_ng-0.6.0/.cargo/config.toml +11 -0
  2. fast_mail_parser_ng-0.6.0/.gitattributes +4 -0
  3. fast_mail_parser_ng-0.6.0/.github/dependabot.yml +13 -0
  4. fast_mail_parser_ng-0.6.0/.github/scripts/check_benchmark.py +78 -0
  5. fast_mail_parser_ng-0.6.0/.github/workflows/publish.yml +217 -0
  6. fast_mail_parser_ng-0.6.0/.github/workflows/test.yml +222 -0
  7. fast_mail_parser_ng-0.6.0/.gitignore +11 -0
  8. fast_mail_parser_ng-0.6.0/CHANGELOG.md +104 -0
  9. fast_mail_parser_ng-0.6.0/CONTRIBUTING.md +190 -0
  10. fast_mail_parser_ng-0.6.0/Cargo.lock +188 -0
  11. fast_mail_parser_ng-0.6.0/Cargo.toml +44 -0
  12. fast_mail_parser_ng-0.6.0/LICENSE +201 -0
  13. fast_mail_parser_ng-0.6.0/MANIFEST.in +2 -0
  14. fast_mail_parser_ng-0.6.0/Makefile +13 -0
  15. fast_mail_parser_ng-0.6.0/PKG-INFO +149 -0
  16. fast_mail_parser_ng-0.6.0/Readme.md +116 -0
  17. fast_mail_parser_ng-0.6.0/SECURITY.md +19 -0
  18. fast_mail_parser_ng-0.6.0/deny.toml +48 -0
  19. fast_mail_parser_ng-0.6.0/fast_mail_parser/__init__.py +8 -0
  20. fast_mail_parser_ng-0.6.0/fast_mail_parser/__init__.pyi +43 -0
  21. fast_mail_parser_ng-0.6.0/fast_mail_parser/py.typed +0 -0
  22. fast_mail_parser_ng-0.6.0/pyproject.toml +42 -0
  23. fast_mail_parser_ng-0.6.0/ruff.toml +12 -0
  24. fast_mail_parser_ng-0.6.0/rust-toolchain.toml +24 -0
  25. fast_mail_parser_ng-0.6.0/src/fast_mail_parser.rs +153 -0
  26. fast_mail_parser_ng-0.6.0/src/mail_parser.rs +151 -0
  27. fast_mail_parser_ng-0.6.0/tests/benchmark/test_read_message.py +21 -0
  28. fast_mail_parser_ng-0.6.0/tests/conftest.py +49 -0
  29. fast_mail_parser_ng-0.6.0/tests/data/attachment_message.eml +33 -0
  30. fast_mail_parser_ng-0.6.0/tests/data/invalid_message.eml +2283 -0
  31. fast_mail_parser_ng-0.6.0/tests/data/large_message.eml +10090 -0
  32. fast_mail_parser_ng-0.6.0/tests/data/rfc/base64_body.eml +10 -0
  33. fast_mail_parser_ng-0.6.0/tests/data/rfc/empty_body.eml +10 -0
  34. fast_mail_parser_ng-0.6.0/tests/data/rfc/folded_header.eml +15 -0
  35. fast_mail_parser_ng-0.6.0/tests/data/rfc/multipart_alternative.eml +22 -0
  36. fast_mail_parser_ng-0.6.0/tests/data/rfc/multipart_mixed_attachment.eml +24 -0
  37. fast_mail_parser_ng-0.6.0/tests/data/rfc/multipart_related.eml +25 -0
  38. fast_mail_parser_ng-0.6.0/tests/data/rfc/nested_multipart.eml +35 -0
  39. fast_mail_parser_ng-0.6.0/tests/data/rfc/quoted_printable_body.eml +10 -0
  40. fast_mail_parser_ng-0.6.0/tests/data/rfc/rfc2047_encoded_subject.eml +10 -0
  41. fast_mail_parser_ng-0.6.0/tests/data/rfc/rfc2231_param_filename.eml +24 -0
  42. fast_mail_parser_ng-0.6.0/tests/data/rfc/rfc5322_plain.eml +10 -0
  43. fast_mail_parser_ng-0.6.0/tests/data/rfc/rfc6532_utf8_headers.eml +10 -0
  44. fast_mail_parser_ng-0.6.0/tests/data/rfc/utf8_8bit_body.eml +10 -0
  45. fast_mail_parser_ng-0.6.0/tests/data/valid_message.eml +2283 -0
  46. fast_mail_parser_ng-0.6.0/tests/generate_rfc_corpus.py +206 -0
  47. fast_mail_parser_ng-0.6.0/tests/test_attachments.py +20 -0
  48. fast_mail_parser_ng-0.6.0/tests/test_contents.py +17 -0
  49. fast_mail_parser_ng-0.6.0/tests/test_contract.py +143 -0
  50. fast_mail_parser_ng-0.6.0/tests/test_correctness.py +195 -0
  51. fast_mail_parser_ng-0.6.0/tests/test_decode_errors.py +75 -0
  52. fast_mail_parser_ng-0.6.0/tests/test_dos_limits.py +82 -0
  53. fast_mail_parser_ng-0.6.0/tests/test_empty_fields.py +75 -0
  54. fast_mail_parser_ng-0.6.0/tests/test_headers.py +17 -0
  55. fast_mail_parser_ng-0.6.0/tests/test_multivalue_headers.py +95 -0
  56. fast_mail_parser_ng-0.6.0/tests/test_rfc_corpus.py +176 -0
@@ -0,0 +1,11 @@
1
+ [target.x86_64-apple-darwin]
2
+ rustflags = [
3
+ "-C", "link-arg=-undefined",
4
+ "-C", "link-arg=dynamic_lookup",
5
+ ]
6
+
7
+ [target.aarch64-apple-darwin]
8
+ rustflags = [
9
+ "-C", "link-arg=-undefined",
10
+ "-C", "link-arg=dynamic_lookup",
11
+ ]
@@ -0,0 +1,4 @@
1
+ # RFC test fixtures are parsed byte-for-byte and use CRLF (the RFC 5322 line
2
+ # ending). Keep them verbatim so Git's autocrlf never rewrites the bytes the
3
+ # tests assert against.
4
+ tests/data/rfc/*.eml -text
@@ -0,0 +1,13 @@
1
+ version: 2
2
+ updates:
3
+ # Rust crate dependencies (Cargo.toml / Cargo.lock at the repo root).
4
+ - package-ecosystem: "cargo"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+
9
+ # GitHub Actions used by the workflows in .github/workflows.
10
+ - package-ecosystem: "github-actions"
11
+ directory: "/"
12
+ schedule:
13
+ interval: "weekly"
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ """Performance quality gate for CI.
3
+
4
+ Reads a pytest-benchmark JSON report and fails the build unless
5
+ ``fast_mail_parser`` is at least ``BENCH_MIN_SPEEDUP`` times faster than the
6
+ pure-Python ``mail-parser`` baseline.
7
+
8
+ The comparison uses each benchmark's *minimum* time, not the mean: on shared CI
9
+ runners the min is the least noise-prone metric (the cleanest observed run), so
10
+ the gate is reproducible and does not flake. Both libraries are benchmarked on
11
+ the same runner in the same run, so the *ratio* is independent of the runner's
12
+ absolute speed (the README reports ~8x). Getting faster always passes; only a
13
+ regression below the configured floor fails.
14
+
15
+ Usage:
16
+ python check_benchmark.py [benchmark.json]
17
+
18
+ Environment:
19
+ BENCH_MIN_SPEEDUP Minimum required speedup ratio (default: 4.0).
20
+ """
21
+ import json
22
+ import os
23
+ import sys
24
+
25
+
26
+ def min_for(benchmarks, predicate, label):
27
+ matches = [b for b in benchmarks if predicate(b["name"])]
28
+ if not matches:
29
+ sys.exit(f"::error::benchmark for {label} not found in report")
30
+ if len(matches) > 1:
31
+ names = ", ".join(b["name"] for b in matches)
32
+ sys.exit(f"::error::ambiguous benchmark match for {label}: {names}")
33
+ return matches[0]["stats"]["min"]
34
+
35
+
36
+ def main():
37
+ path = sys.argv[1] if len(sys.argv) > 1 else "benchmark.json"
38
+ threshold = float(os.environ.get("BENCH_MIN_SPEEDUP", "4.0"))
39
+
40
+ with open(path) as fh:
41
+ benchmarks = json.load(fh)["benchmarks"]
42
+
43
+ fast = min_for(
44
+ benchmarks, lambda n: "fast_mail_parser" in n, "fast_mail_parser"
45
+ )
46
+ baseline = min_for(
47
+ benchmarks,
48
+ lambda n: "mail_parser" in n and "fast" not in n,
49
+ "mail-parser (baseline)",
50
+ )
51
+
52
+ speedup = baseline / fast
53
+ fast_ms, base_ms = fast * 1e3, baseline * 1e3
54
+
55
+ summary = (
56
+ f"### Benchmark quality gate\n\n"
57
+ f"| Library | Min time | Speedup |\n"
58
+ f"|---|---|---|\n"
59
+ f"| fast_mail_parser | {fast_ms:.3f} ms | {speedup:.2f}x |\n"
60
+ f"| mail-parser (baseline) | {base_ms:.3f} ms | 1.00x |\n\n"
61
+ f"Required floor: **{threshold:.2f}x** — "
62
+ f"{'PASS ✅' if speedup >= threshold else 'FAIL ❌'}\n"
63
+ )
64
+ print(summary)
65
+ step_summary = os.environ.get("GITHUB_STEP_SUMMARY")
66
+ if step_summary:
67
+ with open(step_summary, "a") as fh:
68
+ fh.write(summary)
69
+
70
+ if speedup < threshold:
71
+ sys.exit(
72
+ f"::error::performance regression: fast_mail_parser is only "
73
+ f"{speedup:.2f}x faster than mail-parser (floor is {threshold:.2f}x)"
74
+ )
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()
@@ -0,0 +1,217 @@
1
+ name: Publish
2
+
3
+ # Release builds the multi-platform wheels + sdist and publishes to PyPI.
4
+ # workflow_dispatch builds everything WITHOUT publishing — use it to validate
5
+ # the pipeline (artifacts are uploaded) without cutting a release.
6
+ #
7
+ # Wheels target the CPython stable ABI (cp311-abi3, enabled via the
8
+ # pyo3/abi3-py311 cargo feature): each platform job builds ONE wheel that
9
+ # covers every CPython >= 3.11, including versions released after this build.
10
+ # A new CPython minor requires no changes to this workflow.
11
+ on:
12
+ release:
13
+ types: [created]
14
+ workflow_dispatch:
15
+
16
+ concurrency:
17
+ group: publish-${{ github.ref }}
18
+ cancel-in-progress: false
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ jobs:
24
+ linux:
25
+ runs-on: ${{ matrix.platform.runner }}
26
+ strategy:
27
+ matrix:
28
+ platform:
29
+ - runner: ubuntu-latest
30
+ target: x86_64
31
+ - runner: ubuntu-latest
32
+ target: x86
33
+ - runner: ubuntu-latest
34
+ target: aarch64
35
+ - runner: ubuntu-latest
36
+ target: armv7
37
+ - runner: ubuntu-latest
38
+ target: s390x
39
+ - runner: ubuntu-latest
40
+ target: ppc64le
41
+ steps:
42
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
43
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
44
+ with:
45
+ python-version: 3.x
46
+ - name: Build wheels
47
+ uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
48
+ with:
49
+ # Pinned: see rust-toolchain.toml. 1.98.0 costs this crate ~26%.
50
+ rust-toolchain: 1.97.1
51
+ target: ${{ matrix.platform.target }}
52
+ args: --release --out dist
53
+ sccache: "true"
54
+ manylinux: auto
55
+ - name: Upload wheels
56
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
57
+ with:
58
+ name: wheels-linux-${{ matrix.platform.target }}
59
+ path: dist
60
+
61
+ musllinux:
62
+ runs-on: ${{ matrix.platform.runner }}
63
+ strategy:
64
+ matrix:
65
+ platform:
66
+ - runner: ubuntu-latest
67
+ target: x86_64
68
+ - runner: ubuntu-latest
69
+ target: x86
70
+ - runner: ubuntu-latest
71
+ target: aarch64
72
+ - runner: ubuntu-latest
73
+ target: armv7
74
+ steps:
75
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
76
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
77
+ with:
78
+ python-version: 3.x
79
+ - name: Build wheels
80
+ uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
81
+ with:
82
+ # Pinned: see rust-toolchain.toml. 1.98.0 costs this crate ~26%.
83
+ rust-toolchain: 1.97.1
84
+ target: ${{ matrix.platform.target }}
85
+ args: --release --out dist
86
+ sccache: "true"
87
+ manylinux: musllinux_1_2
88
+ - name: Upload wheels
89
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
90
+ with:
91
+ name: wheels-musllinux-${{ matrix.platform.target }}
92
+ path: dist
93
+
94
+ windows:
95
+ runs-on: ${{ matrix.platform.runner }}
96
+ strategy:
97
+ matrix:
98
+ platform:
99
+ - runner: windows-latest
100
+ target: x64
101
+ - runner: windows-latest
102
+ target: x86
103
+ steps:
104
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
105
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
106
+ with:
107
+ python-version: 3.x
108
+ architecture: ${{ matrix.platform.target }}
109
+ - name: Build wheels
110
+ uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
111
+ with:
112
+ # Pinned: see rust-toolchain.toml. 1.98.0 costs this crate ~26%.
113
+ rust-toolchain: 1.97.1
114
+ target: ${{ matrix.platform.target }}
115
+ args: --release --out dist
116
+ sccache: "true"
117
+ - name: Upload wheels
118
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
119
+ with:
120
+ name: wheels-windows-${{ matrix.platform.target }}
121
+ path: dist
122
+
123
+ macos:
124
+ runs-on: ${{ matrix.platform.runner }}
125
+ strategy:
126
+ matrix:
127
+ platform:
128
+ # Only arm64 macOS wheels. The macos-13 (x86_64) runner is being
129
+ # retired and became unschedulable (its job sat queued until GitHub
130
+ # cancelled the release). x86_64 macOS installs fall back to the sdist.
131
+ - runner: macos-14
132
+ target: aarch64
133
+ steps:
134
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
135
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
136
+ with:
137
+ python-version: 3.x
138
+ - name: Build wheels
139
+ uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
140
+ with:
141
+ # Pinned: see rust-toolchain.toml. 1.98.0 costs this crate ~26%.
142
+ rust-toolchain: 1.97.1
143
+ target: ${{ matrix.platform.target }}
144
+ args: --release --out dist
145
+ sccache: "true"
146
+ - name: Upload wheels
147
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
148
+ with:
149
+ name: wheels-macos-${{ matrix.platform.target }}
150
+ path: dist
151
+
152
+ sdist:
153
+ runs-on: ubuntu-latest
154
+ steps:
155
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
156
+ - name: Build sdist
157
+ uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
158
+ with:
159
+ command: sdist
160
+ args: --out dist
161
+ - name: Upload sdist
162
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
163
+ with:
164
+ name: wheels-sdist
165
+ path: dist
166
+
167
+ release:
168
+ name: Release
169
+ runs-on: ubuntu-latest
170
+ needs: [linux, musllinux, windows, macos, sdist]
171
+ permissions:
172
+ # OIDC token for PyPI Trusted Publishing + artifact signing
173
+ id-token: write
174
+ # Used to upload release artifacts
175
+ contents: write
176
+ # Used to generate artifact attestation
177
+ attestations: write
178
+ steps:
179
+ - name: Collect all artifacts into dist/
180
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
181
+ with:
182
+ path: dist
183
+ merge-multiple: true
184
+ # download-artifact@v8 verifies each artifact's digest against the value
185
+ # upload-artifact@v7 recorded; that pairing can report a spurious
186
+ # mismatch even though the download itself succeeds. Warn instead of
187
+ # failing the release — integrity is still attested by the provenance
188
+ # step below.
189
+ digest-mismatch: warn
190
+ - name: Generate artifact attestation
191
+ uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
192
+ with:
193
+ subject-path: "dist/*"
194
+ # Publish via PyPI Trusted Publishing (OIDC) — no long-lived credentials.
195
+ # OIDC also enables PEP 740 attestations (attestations: true); these are
196
+ # silently IGNORED if an explicit username/password is set, so do NOT add
197
+ # `user`/`password` here.
198
+ #
199
+ # The distribution is `fast-mail-parser-ng` (the import path stays
200
+ # `fast_mail_parser`; see [tool.maturin] module-name in pyproject.toml).
201
+ # Requires a one-time PyPI setup — add a Trusted Publisher for
202
+ # namecheap/fast_mail_parser, workflow "publish.yml", environment blank:
203
+ #
204
+ # before the project exists (pending publisher, creates it on first
205
+ # upload): https://pypi.org/manage/account/publishing/
206
+ # afterwards:
207
+ # https://pypi.org/manage/project/fast-mail-parser-ng/settings/publishing/
208
+ #
209
+ # The `environment:` field there must match this job exactly — this job
210
+ # declares none, so leave it blank.
211
+ - name: Publish to PyPI
212
+ if: ${{ github.event_name == 'release' && startsWith(github.ref, 'refs/tags/') }}
213
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
214
+ with:
215
+ packages-dir: dist
216
+ skip-existing: true
217
+ attestations: true
@@ -0,0 +1,222 @@
1
+ name: Test
2
+
3
+ # Runs on pushes to the default branch AND on every pull request, so the
4
+ # matrix can be used as a required merge gate. (Baseline only ran on push.)
5
+ on:
6
+ push:
7
+ branches: [master]
8
+ pull_request:
9
+ workflow_dispatch:
10
+
11
+ # Cancel superseded runs for the same ref to save runner minutes.
12
+ concurrency:
13
+ group: test-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ # ---------------------------------------------------------------------------
21
+ # Lint. rustfmt, mypy --strict, and ruff are enforced (blocking); clippy
22
+ # stays advisory (continue-on-error) until its debt clears, and each runs
23
+ # regardless of the others:
24
+ # - rustfmt -> blocking (source is rustfmt-clean).
25
+ # - clippy -> flip to blocking after the str->bytes fix (#19).
26
+ # - mypy -> blocking: `mypy --strict fast_mail_parser/` is clean (#42).
27
+ # - ruff -> blocking: `ruff check .` is clean (config in ruff.toml).
28
+ # ---------------------------------------------------------------------------
29
+ lint:
30
+ name: Lint
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
34
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
35
+ with:
36
+ python-version: "3.12"
37
+ - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master
38
+ with:
39
+ # Must match rust-toolchain.toml: that file takes precedence over this
40
+ # input, so requesting `stable` here installs rustfmt/clippy for a
41
+ # toolchain cargo then does not use ("'cargo-fmt' is not installed for
42
+ # the toolchain"). Only an explicit RUSTUP_TOOLCHAIN env var overrides
43
+ # the file — see the cargo audit job.
44
+ toolchain: 1.97.1
45
+ components: rustfmt, clippy
46
+ - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
47
+
48
+ - name: cargo fmt --check
49
+ run: cargo fmt --all -- --check
50
+
51
+ # clippy::cast_possible_truncation pinpoints the str->bytes corruption bug.
52
+ - name: cargo clippy
53
+ if: always()
54
+ run: cargo clippy --all-targets -- -D warnings -W clippy::cast_possible_truncation
55
+ continue-on-error: true
56
+
57
+ # Blocking: `mypy --strict fast_mail_parser/` type-checks clean today.
58
+ - name: mypy --strict
59
+ if: always()
60
+ run: |
61
+ python -m pip install --upgrade pip mypy
62
+ mypy --strict fast_mail_parser/
63
+
64
+ # Blocking: `ruff check .` is clean (config in ruff.toml).
65
+ - name: ruff check
66
+ if: always()
67
+ run: |
68
+ python -m pip install ruff
69
+ ruff check .
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Supply-chain audit. Blocking (no continue-on-error): the dependency stack is
73
+ # current (PyO3 0.29) and the Cargo.lock audits clean, so a new advisory
74
+ # against any dependency should fail the build and gate the PR rather than pass
75
+ # silently. (It was previously advisory only because the stack was stale and a
76
+ # fresh advisory against a pinned dep would have blocked unrelated PRs.)
77
+ # rust-toolchain.toml pins the build to 1.97.1 (see that file) and the MSRV
78
+ # (1.83) is declared via `rust-version` in Cargo.toml; this job sets
79
+ # RUSTUP_TOOLCHAIN=stable explicitly so cargo-audit always audits against
80
+ # current stable regardless of that pin.
81
+ #
82
+ # cargo-audit is `cargo install`ed from source (~3 min — previously the
83
+ # slowest job by far). The compiled binary is cached and keyed on the pinned
84
+ # CARGO_AUDIT_VERSION: a cache hit skips the build (restores in seconds); a
85
+ # miss builds it once and caches it.
86
+ #
87
+ # Cache invalidation: to upgrade cargo-audit, bump CARGO_AUDIT_VERSION below.
88
+ # That installs the new version AND invalidates the cache in one change — the
89
+ # version IS the cache key, so there is no separate suffix to remember. The
90
+ # key is intentionally NOT tied to the rustc/cargo version: the binary runs
91
+ # fine across toolchain updates, and keying on it would force a needless
92
+ # ~3-min rebuild on every stable bump. The RustSec advisory DB is fetched
93
+ # fresh on every run, so findings stay current regardless of the cached
94
+ # binary version.
95
+ # ---------------------------------------------------------------------------
96
+ security-audit:
97
+ name: cargo audit
98
+ runs-on: ubuntu-latest
99
+ env:
100
+ RUSTUP_TOOLCHAIN: stable
101
+ # Bump to upgrade cargo-audit; this also invalidates the binary cache.
102
+ CARGO_AUDIT_VERSION: "0.22.2"
103
+ steps:
104
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
105
+ - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master
106
+ with:
107
+ toolchain: stable
108
+ - name: Cache cargo-audit binary
109
+ id: cache-cargo-audit
110
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
111
+ with:
112
+ path: ~/.cargo/bin/cargo-audit
113
+ key: cargo-audit-${{ runner.os }}-${{ env.CARGO_AUDIT_VERSION }}
114
+ - name: Install cargo-audit
115
+ if: steps.cache-cargo-audit.outputs.cache-hit != 'true'
116
+ run: cargo install cargo-audit --version ${{ env.CARGO_AUDIT_VERSION }} --locked
117
+ - name: cargo audit
118
+ run: cargo audit
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Build once, test everywhere — the real merge gate. A single cp311-abi3
122
+ # wheel is built (stable ABI, see pyo3/abi3-py311 in Cargo.toml), then that
123
+ # SAME wheel is installed and tested on every supported CPython (3.11–3.14).
124
+ # This verifies the abi3 promise instead of assuming it: what ships to PyPI
125
+ # is one wheel per platform, so CI must not rebuild per Python version.
126
+ # ---------------------------------------------------------------------------
127
+ build-wheel:
128
+ name: Build abi3 wheel
129
+ runs-on: ubuntu-latest
130
+ steps:
131
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
132
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
133
+ with:
134
+ python-version: "3.11"
135
+ - name: Build wheel
136
+ uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
137
+ with:
138
+ # Pinned: see rust-toolchain.toml. 1.98.0 costs this crate ~26%.
139
+ rust-toolchain: 1.97.1
140
+ args: --release --out dist
141
+ sccache: "true"
142
+ manylinux: auto
143
+ - name: Upload wheel
144
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
145
+ with:
146
+ name: test-wheel
147
+ path: dist
148
+
149
+ test:
150
+ name: Test (py${{ matrix.python-version }})
151
+ runs-on: ubuntu-latest
152
+ needs: build-wheel
153
+ strategy:
154
+ fail-fast: false
155
+ matrix:
156
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
157
+ steps:
158
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
159
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
160
+ with:
161
+ python-version: ${{ matrix.python-version }}
162
+ cache: pip
163
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
164
+ with:
165
+ name: test-wheel
166
+ path: dist
167
+ # Same upload@v7/download@v8 pairing as publish.yml: digest checks
168
+ # can report a spurious mismatch even though the download succeeds.
169
+ digest-mismatch: warn
170
+ - name: Install the built wheel + test dependencies
171
+ run: pip install "$(ls dist/*.whl)[test]"
172
+ - name: Test
173
+ run: make test
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # Benchmark quality gate. The benchmark used to run inside every matrix cell
177
+ # (6x redundant, slow). It now runs once here and acts as a performance gate:
178
+ # fast_mail_parser must stay at least BENCH_MIN_SPEEDUP times faster than the
179
+ # pure-Python mail-parser baseline (README reports ~8x). The ratio is measured
180
+ # on the same runner in the same run, so it is hardware-independent and stable.
181
+ # Getting faster always passes; only regressions below the floor fail.
182
+ # ---------------------------------------------------------------------------
183
+ benchmark:
184
+ name: Benchmark quality gate
185
+ runs-on: ubuntu-latest
186
+ # PR merge gate only: run on pull_request and workflow_dispatch, skip on
187
+ # push to master — re-running the benchmark post-merge is wasteful (#47).
188
+ if: github.event_name != 'push'
189
+ steps:
190
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
191
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
192
+ with:
193
+ python-version: "3.12"
194
+ cache: pip
195
+ - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master
196
+ with:
197
+ toolchain: 1.97.1
198
+ # Cache the cargo registry and build artifacts so `make install` reuses
199
+ # compiled crates across runs instead of rebuilding from scratch (#47).
200
+ - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
201
+ - name: Install the package + test dependencies
202
+ run: |
203
+ make install
204
+ make install-test
205
+ - name: Run benchmark
206
+ run: pytest -v tests/benchmark --benchmark-min-rounds=25 --benchmark-json=benchmark.json
207
+ - name: Enforce performance gate
208
+ env:
209
+ # Gate on min-time ratio. The optimized parser measures ~7.8–8.7x on
210
+ # CI; the ratio swings with the runner's CPU (the pure-Python baseline
211
+ # scales differently than the Rust extension), so the README's ~8x is
212
+ # the typical figure, not a hard floor. Gate at 7x — below the observed
213
+ # range so legitimate (perf-neutral) PRs don't flake, while still
214
+ # catching real regressions.
215
+ BENCH_MIN_SPEEDUP: "7.0"
216
+ run: python .github/scripts/check_benchmark.py benchmark.json
217
+ - name: Upload benchmark report
218
+ if: always()
219
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
220
+ with:
221
+ name: benchmark-json
222
+ path: benchmark.json
@@ -0,0 +1,11 @@
1
+ /target
2
+ .idea
3
+ .venv/
4
+ venv/
5
+ fast_mail_parser.egg-info
6
+ .eggs
7
+ build
8
+ .pytest_cache
9
+ dist
10
+ __pycache__
11
+ .python-version
@@ -0,0 +1,104 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.6.0] - 2026-08-26
11
+
12
+ ### Changed
13
+
14
+ - **The distribution is now published as `fast-mail-parser-ng`.** Install with
15
+ `pip install fast-mail-parser-ng`. The import path is unchanged — existing
16
+ code keeps working as-is:
17
+
18
+ ```python
19
+ from fast_mail_parser import parse_email, ParseError
20
+ ```
21
+
22
+ Only the name in your requirements file changes. No code in this release
23
+ differs from 0.5.0; the version bump signals that consumers must update how
24
+ they install the package.
25
+
26
+ The `fast-mail-parser` name on PyPI still points at an unmaintained 0.2.5
27
+ from June 2022, published by the library's original author before he left
28
+ Namecheap. We do not control that name: the PEP 541 transfer request
29
+ ([pypi/support#11044](https://github.com/pypi/support/issues/11044)) has been
30
+ open and unattended since 2026-06-13. Rather than block releases on that
31
+ queue indefinitely, this repository publishes under a name we own. If the
32
+ transfer is ever granted, `fast-mail-parser` will resume as an alias.
33
+ - Wheels are now built with a pinned Rust toolchain (1.97.1). rustc 1.98.0
34
+ makes the parser ~26% slower, so the pin keeps 0.6.0's wheels as fast as
35
+ 0.5.0's (#119, tracked in #120).
36
+
37
+ ## [0.5.0] - 2026-08-03
38
+
39
+ ### Changed
40
+
41
+ - Wheels now target the CPython stable ABI (`cp311-abi3`, via the
42
+ `pyo3/abi3-py311` feature): a single wheel per platform supports every
43
+ CPython ≥ 3.11, including versions released after the build. New CPython
44
+ minors no longer require a repo change or a new release for installability
45
+ (#14, #15, #101). The abi3 build benchmarked ~11% *faster* than the
46
+ version-specific build (min parse time, CPython 3.12, Apple Silicon), so no
47
+ hybrid version-specific wheels are shipped.
48
+ - CI now builds one abi3 wheel and runs the full test matrix (CPython
49
+ 3.11–3.14) against that same wheel — the stable-ABI contract is verified,
50
+ not assumed. The per-version publish matrix collapsed to one wheel per
51
+ platform.
52
+
53
+ ## [0.4.0] - 2026-06-12
54
+
55
+ ### Breaking
56
+
57
+ - Dropped support for Python 3.7–3.10; the minimum supported version is now
58
+ **3.11** (`requires-python >= 3.11`).
59
+ - `str` input to `parse_email` is now decoded as UTF-8 (lossless). Previously
60
+ each code point was truncated to its low byte, corrupting non-ASCII input.
61
+ Output for non-ASCII `str` therefore changes — pass `bytes` for exact control.
62
+ - Message bodies that fail to decode (e.g. invalid base64) now raise
63
+ `ParseError` instead of silently returning an empty value.
64
+
65
+ ### Changed
66
+
67
+ - Upgraded PyO3 0.16.6 → 0.29.0, resolving RUSTSEC-2025-0020 and
68
+ RUSTSEC-2026-0177.
69
+ - Upgraded `mailparse` 0.15.0 → 0.16.1.
70
+ - Track the stable Rust toolchain and declare the MSRV (`rust-version = 1.83`).
71
+ - Faster string-input parsing via a UTF-8 fast path.
72
+
73
+ ### Added
74
+
75
+ - Support for CPython 3.13 and 3.14.
76
+ - Denial-of-service hardening: input-size cap (100 MiB) and MIME
77
+ recursion-depth cap (256), both surfaced as `ParseError`.
78
+ - Public API contract tests, an RFC-feature `.eml` corpus, round-trip
79
+ correctness tests, and an empty-field sentinel test.
80
+ - `CONTRIBUTING.md` with build-from-source and testing instructions.
81
+
82
+ ### Security
83
+
84
+ - Fixed the lossy `str`→bytes conversion that corrupted non-ASCII input.
85
+ - Added untrusted-input DoS guards (input-size and recursion-depth caps).
86
+ - Hardened CI: PR-gated matrix, blocking `cargo audit`, SHA-pinned actions,
87
+ Dependabot, `cargo-deny`, OIDC Trusted Publishing, and removed real PII from
88
+ test fixtures.
89
+
90
+ ## [0.3.0]
91
+
92
+ Prior release (PyO3 0.16.6). See the Git history for details.
93
+
94
+ ---
95
+
96
+ The package version is single-sourced from `Cargo.toml`'s `[package].version`.
97
+ `pyproject.toml` declares `dynamic = ["version"]`, so maturin reads the version
98
+ from `Cargo.toml` at build time. Bump the version in `Cargo.toml` only.
99
+
100
+ [Unreleased]: https://github.com/namecheap/fast_mail_parser/compare/v0.6.0...HEAD
101
+ [0.6.0]: https://github.com/namecheap/fast_mail_parser/compare/v0.5.0...v0.6.0
102
+ [0.5.0]: https://github.com/namecheap/fast_mail_parser/compare/v0.4.0...v0.5.0
103
+ [0.4.0]: https://github.com/namecheap/fast_mail_parser/compare/v0.3.0...v0.4.0
104
+ [0.3.0]: https://github.com/namecheap/fast_mail_parser/releases/tag/v0.3.0