The canonical JWZ email message-threading algorithm with RFC 5322 identification-field parsing — pure stdlib, no runtime dependencies.
Project description
threadweave
The canonical JWZ email message-threading algorithm for Python — pure stdlib, zero runtime dependencies.
threadweave turns a flat list of email messages into conversation trees using
Jamie Zawinski's threading algorithm,
built on top of RFC 5322 §3.6.4 identification-field parsing (Message-ID,
References, In-Reply-To). It is the algorithm every serious mail client uses
to group a mailbox into threads.
It exists because the obviously-wrong approaches — grouping by subject alone, or
naively chaining In-Reply-To — mis-thread real mail: subjects collide across
unrelated conversations, references arrive out of order, roots go missing, and
malformed headers form reference loops. JWZ handles all of these; threadweave
implements it faithfully and loop-safely.
What it does
- Builds the JWZ id-table of containers and links
Referenceschains without creating loops and without overriding a message's good existing parent. - Recovers missing roots as empty placeholder containers, then prunes empty containers correctly (nuking childless empties and splice-promoting the children of empty ones, with the special root-level single-child handling).
- Optionally groups the root set by base subject (
Re:/Fwd:/Fw:stripped) — a heuristic, off by default. - Terminates on adversarial input: self-references and mutual reference cycles never loop or crash.
Install
pip install threadweave
No runtime dependencies — only the standard library (re, hashlib,
dataclasses, typing).
Quickstart
from threadweave import Message, thread_messages
threads = thread_messages([
Message(message_id="a", subject="Deploy plan"),
Message(message_id="b", in_reply_to="a", references=["a"], subject="Re: Deploy plan"),
Message(message_id="c", references=["a", "b"], subject="Re: Deploy plan"),
])
assert len(threads) == 1 # one conversation
root = threads[0] # a Container
print(root.message.message_id) # "a"
for node in root.iter_descendants():
print(node.message.message_id) # "b", then "c"
Group unrelated-but-same-subject roots when references are missing:
threads = thread_messages(messages, group_by_subject=True)
API
| Symbol | Purpose |
|---|---|
Message |
Input dataclass: message_id, in_reply_to, references, subject, payload. |
thread_messages(messages, *, group_by_subject=False) |
Run the JWZ algorithm; returns the root Container list. |
Container |
Thread-tree node: message, parent, children, is_empty, add_child, iter_descendants. |
normalize_message_id / extract_reference_ids |
RFC 5322 Message-ID / References parsing. |
generate_email_fingerprint |
Deterministic SHA-256 identity for messages lacking a usable Message-ID. |
normalize_subject / is_reply_subject |
Base-subject stripping and reply detection. |
Container.iter_descendants and the internal linking are loop-safe: traversal
visits each node at most once, so a cyclic reference graph can never hang.
One source, multi use (OSMU)
The RFC 5322 header primitives in
threadweave/headers.py are extracted
behaviour-preserving from a production control plane
(naruon), where they normalize
Message-ID/References headers for canonical email threading. The JWZ
assembly here is a fresh canonical implementation built on top of those
primitives — one source, usable both as a standalone dependency and as a git
submodule.
Research grounding
See docs/research: Zawinski's threading algorithm
(https://www.jwz.org/doc/threading.html) and RFC 5322 §3.6.4 (Identification
Fields). Base-subject grouping is documented there as a heuristic fallback.
License
Apache-2.0. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file threadweave-0.1.0.tar.gz.
File metadata
- Download URL: threadweave-0.1.0.tar.gz
- Upload date:
- Size: 15.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ee7c01bf3855fa12ed0d28cbec4f4ba31c405801b535d2931c5870dfe467f98
|
|
| MD5 |
275b514f1cf75ef4f15a6cb2115f242c
|
|
| BLAKE2b-256 |
2b82878a0a39183e2e0e084c875d77160dd697f06f013d231ef5dbdf57865d8c
|
File details
Details for the file threadweave-0.1.0-py3-none-any.whl.
File metadata
- Download URL: threadweave-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
03c31fa21873a9493687d81eab4ec067bf169dade7cff077b80df46fd0db3aaf
|
|
| MD5 |
58e9136b31260c0d901100f3fbc286a7
|
|
| BLAKE2b-256 |
60e0ffbc0d61d68304602120998a5d660c8108464064bdedc814dc4be8410425
|