jwz-go groups a flat list of email headers into conversation threads using
Jamie Zawinski's algorithm — the same
threading model that ships in Mozilla Thunderbird and most modern MUAs.
It joins messages by Message-ID / In-Reply-To / References, and falls
back to a locale-aware subject match (Re:, Fwd:, AW:, SV:, RV:,
Odp:, …) when reference data is missing.
- Pure Go, zero dependencies. Drop-in for any mail client, archive viewer, or mailing-list tool.
- Deterministic output. Threads sort newest-first by
LatestAtwith a stable EmailID tiebreaker. - Locale-aware subject fallback. Groups orphans across
Re:,Fwd:,AW:,SV:,RV:,Odp:,Antw:and more. - Loop-safe. Cycle detection in
link()prevents pathological reply chains from blowing the stack. - Application-defined IDs. Carry your own row/UID through the tree via
EmailID— no extra lookup needed.
go get github.com/floatpane/jwz-goRequires Go 1.26+.
package main
import (
"fmt"
"time"
"github.com/floatpane/jwz-go"
)
func main() {
base := time.Now()
threads := jwz.Build([]jwz.EmailHeader{
{ID: "<a@example>", Subject: "Release plan", Date: base, EmailID: "1", Sender: "alice"},
{ID: "<b@example>", InReplyTo: "<a@example>",
Subject: "Re: Release plan", Date: base.Add(time.Minute), EmailID: "2", Sender: "bob"},
{ID: "<c@example>", References: []string{"<a@example>", "<b@example>"},
Subject: "Re: Re: Release plan", Date: base.Add(2*time.Minute), EmailID: "3", Sender: "carol"},
})
for _, t := range threads {
fmt.Printf("%s (%d messages, last %s)\n", t.Subject, t.Count, t.LatestAt)
}
}| Type | Description |
|---|---|
EmailHeader |
Input. Subset of RFC 5322 headers required for threading. |
Thread |
Output. Conversation root + aggregate metadata. |
ThreadNode |
One message inside a Thread, with children. |
CanonicalSubject(s) is exported for callers that want the same subject
normalization rules outside of threading — e.g. for search dedup or
mailing-list digest collapse.
jwz.CanonicalSubject("Re: AW: SV: Release plan") // -> "release plan"- Parse references. Each header's
Referenceslist is walked left→right; every consecutive pair is linked parent→child. - Resolve parent.
In-Reply-Towins; otherwise the lastReferencesentry. - Prune empty containers. Tree is collapsed so that placeholder nodes with no real message disappear unless they have multiple children (which we keep, so siblings stay siblings).
- Subject grouping. Roots that share a canonical subject are merged under one tree — this catches forwarded threads and clients that drop References.
- Sort. Children by Date (stable); threads newest-first by
LatestAt.
Full API reference: pkg.go.dev/github.com/floatpane/jwz-go
PRs welcome. See CONTRIBUTING.md.
Report vulnerabilities privately via SECURITY.md.
MIT. See LICENSE.