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.
- fast_mail_parser_ng-0.6.0/.cargo/config.toml +11 -0
- fast_mail_parser_ng-0.6.0/.gitattributes +4 -0
- fast_mail_parser_ng-0.6.0/.github/dependabot.yml +13 -0
- fast_mail_parser_ng-0.6.0/.github/scripts/check_benchmark.py +78 -0
- fast_mail_parser_ng-0.6.0/.github/workflows/publish.yml +217 -0
- fast_mail_parser_ng-0.6.0/.github/workflows/test.yml +222 -0
- fast_mail_parser_ng-0.6.0/.gitignore +11 -0
- fast_mail_parser_ng-0.6.0/CHANGELOG.md +104 -0
- fast_mail_parser_ng-0.6.0/CONTRIBUTING.md +190 -0
- fast_mail_parser_ng-0.6.0/Cargo.lock +188 -0
- fast_mail_parser_ng-0.6.0/Cargo.toml +44 -0
- fast_mail_parser_ng-0.6.0/LICENSE +201 -0
- fast_mail_parser_ng-0.6.0/MANIFEST.in +2 -0
- fast_mail_parser_ng-0.6.0/Makefile +13 -0
- fast_mail_parser_ng-0.6.0/PKG-INFO +149 -0
- fast_mail_parser_ng-0.6.0/Readme.md +116 -0
- fast_mail_parser_ng-0.6.0/SECURITY.md +19 -0
- fast_mail_parser_ng-0.6.0/deny.toml +48 -0
- fast_mail_parser_ng-0.6.0/fast_mail_parser/__init__.py +8 -0
- fast_mail_parser_ng-0.6.0/fast_mail_parser/__init__.pyi +43 -0
- fast_mail_parser_ng-0.6.0/fast_mail_parser/py.typed +0 -0
- fast_mail_parser_ng-0.6.0/pyproject.toml +42 -0
- fast_mail_parser_ng-0.6.0/ruff.toml +12 -0
- fast_mail_parser_ng-0.6.0/rust-toolchain.toml +24 -0
- fast_mail_parser_ng-0.6.0/src/fast_mail_parser.rs +153 -0
- fast_mail_parser_ng-0.6.0/src/mail_parser.rs +151 -0
- fast_mail_parser_ng-0.6.0/tests/benchmark/test_read_message.py +21 -0
- fast_mail_parser_ng-0.6.0/tests/conftest.py +49 -0
- fast_mail_parser_ng-0.6.0/tests/data/attachment_message.eml +33 -0
- fast_mail_parser_ng-0.6.0/tests/data/invalid_message.eml +2283 -0
- fast_mail_parser_ng-0.6.0/tests/data/large_message.eml +10090 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/base64_body.eml +10 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/empty_body.eml +10 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/folded_header.eml +15 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/multipart_alternative.eml +22 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/multipart_mixed_attachment.eml +24 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/multipart_related.eml +25 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/nested_multipart.eml +35 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/quoted_printable_body.eml +10 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/rfc2047_encoded_subject.eml +10 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/rfc2231_param_filename.eml +24 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/rfc5322_plain.eml +10 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/rfc6532_utf8_headers.eml +10 -0
- fast_mail_parser_ng-0.6.0/tests/data/rfc/utf8_8bit_body.eml +10 -0
- fast_mail_parser_ng-0.6.0/tests/data/valid_message.eml +2283 -0
- fast_mail_parser_ng-0.6.0/tests/generate_rfc_corpus.py +206 -0
- fast_mail_parser_ng-0.6.0/tests/test_attachments.py +20 -0
- fast_mail_parser_ng-0.6.0/tests/test_contents.py +17 -0
- fast_mail_parser_ng-0.6.0/tests/test_contract.py +143 -0
- fast_mail_parser_ng-0.6.0/tests/test_correctness.py +195 -0
- fast_mail_parser_ng-0.6.0/tests/test_decode_errors.py +75 -0
- fast_mail_parser_ng-0.6.0/tests/test_dos_limits.py +82 -0
- fast_mail_parser_ng-0.6.0/tests/test_empty_fields.py +75 -0
- fast_mail_parser_ng-0.6.0/tests/test_headers.py +17 -0
- fast_mail_parser_ng-0.6.0/tests/test_multivalue_headers.py +95 -0
- fast_mail_parser_ng-0.6.0/tests/test_rfc_corpus.py +176 -0
|
@@ -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,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
|