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.
Metadata
Release files for threadweave 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| threadweave-0.1.0.tar.gz | 15.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| threadweave-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.5 kB
Release files / threadweave-0.1.0.tar.gz
| Download URL | threadweave-0.1.0.tar.gz |
|---|---|
| Size | 15.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8ee7c01bf3855fa12ed0d28cbec4f4ba31c405801b535d2931c5870dfe467f98
|
|
BLAKE2b-256 checksum How to use checksums |
2b82878a0a39183e2e0e084c875d77160dd697f06f013d231ef5dbdf57865d8c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Release files / threadweave-0.1.0-py3-none-any.whl
| Download URL | threadweave-0.1.0-py3-none-any.whl |
|---|---|
| Size | 15.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
03c31fa21873a9493687d81eab4ec067bf169dade7cff077b80df46fd0db3aaf
|
|
BLAKE2b-256 checksum How to use checksums |
60e0ffbc0d61d68304602120998a5d660c8108464064bdedc814dc4be8410425
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|