Files
inwit e86a33efdf jwz: fix nil dereference when threading maildir messages
pruneEmptyContainers recurses into a container's children (case 3) but
never re-checks the container afterwards. If the container is
a placeholder (threadable == nil) and the recursion clears all its
children, it becomes an empty node that remains in the root set.
gatherSubjects then crashes accessing c.child.threadable.

Fix by checking after the recursive call: if the container now has no
threadable and no children, remove it from the parent's list using the
same pattern as case 1.

Fixes: https://todo.sr.ht/~rjarry/aerc/353
Changelog-fixed: Crash when threading maildir messages with references
 that create empty dummy containers in the root set.
Reported-by: Robert Vojta <rovojta@gmail.com>
Signed-off-by: inwit <inwit@sindominio.net>
Acked-by: Robin Jarry <robin@jarry.cc>
2026-07-26 14:30:17 +02:00

674 lines
19 KiB
Go

// Package jwz is an implementation of the email threading algorithm created by Jamie Zawinski and explained by him
// at: https://www.jwz.org/doc/threading.html
//
// This package was created by cribbing from the code at:
//
// https://www.jwz.org/doc/threading.html#:~:text=grendel-1999-05-14.tar.gz
//
// from the Java source code in view/Threader.java - it contains no ham and cheese sandwiches.
//
// The code, interface etc. was obviously adapted in to Go form, though where possible, the code reflects the
// original Java if it is not too ungolike.
//
// Author: Jim Idle - jimi@idle.ws / jimi@gatherstars.com
// SPDX-License-Identifier: Apache-2.0
//
// See the LICENSE file, sit down, have a scone.
package jwz
import (
"errors"
"fmt"
)
// Threader arranges a set of messages into a thread hierarchy, by references.
type Threader struct {
rootNode *threadContainer
idTable map[string]*threadContainer
bogusIDCount int
}
// NewThreader returns an instance of the Threader struct, that is ready to attack
// your Threadable
//
//goland:noinspection GoUnusedExportedFunction
func NewThreader() *Threader {
t := &Threader{
idTable: make(map[string]*threadContainer),
}
return t
}
// Thread will create a threadable organized so that the root node
// is the original reference, creating dummy placeholders for the emails
// we don't have yet
func (t *Threader) Thread(threadable Threadable) (Threadable, error) {
if threadable == nil {
return nil, nil
}
// Build a thread container from this single email
//
if !threadable.IsDummy() {
if err := t.buildContainer(threadable); err != nil {
return nil, err
}
} else {
return nil, errors.New("cannot thread a single email with a dummy root")
}
var err error
// Organize the root set from what we have
//
t.rootNode, err = t.findRootSet()
if err != nil {
return nil, err
}
// We no longer need the map - probably no real need to blank it here, but the original Java code did that,
// and it won't harm to let the GC reclaim this in case our caller keeps the *Threader around for some reason
//
t.idTable = nil
// We do this to avoid flipping the input order each time through.
//
t.rootNode.reverseChildren()
// There should not be a next in the root of a conversation thread
//
if t.rootNode.next != nil {
return nil, fmt.Errorf("root node contains a next and should not: %#v", t.rootNode)
}
// Because the result of this function is a tree that does actually contain dummies for missing references
// we need to add a dummy threadable for any node that does not yet have one. Then we can flush the chain
// of containers in to the threadable
//
t.rootNode.fillDummy(threadable)
var result Threadable
if t.rootNode.child != nil {
result = t.rootNode.child.threadable
}
// Flush the tree structure of each element of the root set down into
// their underlying Threadables
//
_ = t.rootNode.flush()
t.rootNode = nil
return result, nil
}
// ThreadSlice will thread the set of messages contained within threadableSlice.
// The Threadable returned is the new first element of the root set.
func (t *Threader) ThreadSlice(threadableSlice []Threadable) (Threadable, error) {
if len(threadableSlice) == 0 {
return nil, nil
}
// Iterate all the Threadable represented by the root and build the
// threadContainer from them
//
for _, nt := range threadableSlice {
if !nt.IsDummy() {
if err := t.buildContainer(nt); err != nil {
return nil, err
}
}
}
return t.threadRoot()
}
// ThreadRoot will thread the set of messages provided by ThreadableRoot.
// The Threadable returned is the new first element of the root set.
func (t *Threader) ThreadRoot(threadableRoot ThreadableRoot) (Threadable, error) {
if threadableRoot == nil {
return nil, nil
}
// Iterate all the Threadable represented by the root and build the
// threadContainer from them
//
for threadableRoot.Next() {
nt := threadableRoot.Get()
if !nt.IsDummy() {
if err := t.buildContainer(nt); err != nil {
return nil, err
}
}
}
return t.threadRoot()
}
func (t *Threader) threadRoot() (Threadable, error) {
var err error
// Organize the root set from what we have
//
t.rootNode, err = t.findRootSet()
if err != nil {
return nil, err
}
// We no longer need the map - probably no real need to blank it here, but the original Java code did that,
// and it won't harm to let the GC reclaim this in case our caller keeps the *Threader around for some reason
//
t.idTable = nil
// Get rid of any empty containers. They should no longer needed
//
t.pruneEmptyContainers(t.rootNode)
// We do this so to avoid flipping the input order each time through.
//
t.rootNode.reverseChildren()
// We might need to sort on subjects, so let's process them
//
t.gatherSubjects()
// There should not be a next in the root of a conversation thread
//
if t.rootNode.next != nil {
return nil, fmt.Errorf("root node contains a next and should not: %#v", t.rootNode)
}
for r := t.rootNode.child; r != nil; r = r.next {
// If this direct child of the root node has no threadable in it,
// manufacture a dummy container to bind its children together.
// Note that these dummies can only ever occur as elements of
// the root set.
//
if r.threadable == nil {
r.threadable = r.child.threadable.MakeDummy(r.forID)
}
}
var result Threadable
if t.rootNode.child != nil {
result = t.rootNode.child.threadable
}
// Flush the tree structure of each element of the root set down into
// their underlying Threadables
//
_ = t.rootNode.flush()
t.rootNode = nil
return result, nil
}
// buildContainer() does three things:
//
// - It walks the tree of Threadable, and wraps each in a
// threadContainer object.
// - It indexes each threadContainer object in the idTable, under
// the message ID of the contained Threadable.
// - For each of the references within Threadable, it ensures that there
// is a threadContainer in the table (an empty one, if necessary.)
func (t *Threader) buildContainer(threadable Threadable) error {
var present bool
// See if we already have a container for this threadable
//
id := threadable.MessageThreadID()
tid := id
c, present := t.idTable[id]
if present {
// There is already a ThreadContainer in the table for this ID.
// Under normal circumstances, there will be no IThreadable in it
// (since it was a forward reference from a References field.)
//
// If there is already a threadable in it, then that means there
// are two IThreadables with the same ID. Generate a new ID for
// this one, sigh... This ID is only used to cause the two entries
// in the hash table to not stomp each other.
//
if c.threadable != nil {
id = fmt.Sprintf("<Bogus-id:%d>", t.bogusIDCount)
t.bogusIDCount++
c = nil
} else {
c.threadable = threadable
}
}
// Create a ThreadContainer for this Threadable, and store it in
// the map
//
if c == nil {
c = &threadContainer{forID: tid}
c.threadable = threadable
c.forID = tid
t.idTable[id] = c
}
// Create ThreadContainers for each of the references which don't
// have them. Link each of the referenced messages together in the
// order implied by the references field, unless they are already
// linked.
//
var parentRef, ref *threadContainer
// Iterate through the references field of the threadable and see if we
// already have a reference to them in our map. Create one if not
//
refs := threadable.MessageThreadReferences()
for _, refString := range refs {
ref, present = t.idTable[refString]
if !present {
ref = &threadContainer{forID: refString}
t.idTable[refString] = ref
}
// If we have references A B C D, make D be a child of C, etc.,
// except if they have parents already.
//
if parentRef != nil && // there is a parent
ref.parent == nil && // don't have a parent already
parentRef != ref && // not a tight loop
!ref.findChild(parentRef) && // already linked
!parentRef.findChild(ref) { // not a wide loop
// Ok, link it into the parent's child list.
//
ref.parent = parentRef
ref.next = parentRef.child
parentRef.child = ref
}
parentRef = ref
}
// At this point `parentRef' is set to the container of the last element
// in the references field. Make that be the parent of this container,
// unless doing so would introduce a circularity.
//
if parentRef != nil &&
(parentRef == c ||
c.findChild(parentRef)) {
parentRef = nil
}
if c.parent != nil {
// If it has a parent already, that's there because we saw this message
// in a references field, and presumed a parent based on the other
// entries in that field. Now that we have the actual message, we can
// be more definitive, so throw away the old parent and use this new one.
// Find this container in the parent's child-list, and unlink it.
//
// Note that this could cause this message to now have no parent, if it
// has no references field, but some message referred to it as the
// non-first element of its references. (Which would have been some
// kind of lie...)
//
var rest, prev *threadContainer
for prev, rest = nil, c.parent.child; rest != nil; {
if rest == c {
break
}
prev = rest
rest = rest.next
}
if rest == nil {
return fmt.Errorf("didn't find %#v in parent %#v", c, c.parent)
}
if prev == nil {
c.parent.child = c.next
} else {
prev.next = c.next
}
c.next = nil
c.parent = nil
}
// If we have a parent, link c into the parent's child list.
//
if parentRef != nil {
c.parent = parentRef
c.next = parentRef.child
parentRef.child = c
}
// No error
//
return nil
}
// findRootSet finds the root set of the threadContainers, and returns a root node.
//
// NB: A container is in the root set if it has no parents.
func (t *Threader) findRootSet() (*threadContainer, error) {
root := &threadContainer{}
for _, c := range t.idTable {
if c.parent == nil {
if c.next != nil {
return nil, fmt.Errorf("container has no parent, but has a next value: %#v", c.next)
}
c.next = root.child
root.child = c
}
}
return root, nil
}
// Walk through the threads and discard any empty container objects.
// After calling this, there will only be any empty container objects
// at depth 0, and those will all have at least two kids.
func (t *Threader) pruneEmptyContainers(parent *threadContainer) {
var prev *threadContainer
container := parent.child
var next *threadContainer
if container != nil {
next = container.next
}
for container != nil {
switch {
case container.threadable == nil && container.child == nil:
// This is an empty container with no kids. Nuke it.
//
// Normally such containers won't occur, but they can show up when
// two messages have References lines that disagree. For example,
// assuming A and B are messages, and 1, 2, and 3 are references for
// messages we haven't seen:
//
// A has refs: 1 2 3
// B has refs: 1 3
//
// There is ambiguity whether 3 is a child of 1 or 2. So,
// depending on the processing order, we might end up with either
//
// -- 1
// |-- 2
// |-- 3
// |-- A
// |-- B
// or
// -- 1
// |-- 2 <--- non-root childless container
// |-- 3
// |-- A
// |-- B
//
if prev == nil {
parent.child = container.next
} else {
prev.next = container.next
}
// Set container to prev so that prev keeps its same value
// the next time through the loop.
//
container = prev
case container.threadable == nil && // expired, and
container.child != nil && // has kids, and
(container.parent != nil || // not at root, or
container.child.next == nil):
// Expired message with kids. Promote the kids to this level.
// Don't do this if we would be promoting them to the root level,
// unless there is only one kid.
//
var tail *threadContainer
kids := container.child
// Remove this container from the list, replacing it with `kids'
//
if prev == nil {
parent.child = kids
} else {
prev.next = kids
}
// Make each child's parent be this level's parent.
// Make the last child's next be this container's next
// - splicing `kids' into the list in place of `container'
//
for tail = kids; tail.next != nil; tail = tail.next {
tail.parent = container.parent
}
tail.parent = container.parent
tail.next = container.next
// Since we've inserted items in the chain, `next' currently points
// to the item after them (tail.next); reset that so that we process
// the newly promoted items the very next time around.
//
next = kids
// Set container to prev so that prev keeps its same value
// the next time through the loop.
//
container = prev
case container.child != nil:
// A container with children.
// Iterate over its children, and try to strip out the junk.
//
t.pruneEmptyContainers(container)
// The recursion may have cleared all of a dummy's children.
// If so, remove the now-empty container.
//
if container.threadable == nil && container.child == nil {
if prev == nil {
parent.child = container.next
} else {
prev.next = container.next
}
container = prev
}
}
// Set up for the next iteration
//
prev = container
container = next
if container == nil {
next = nil
} else {
next = container.next
}
}
}
// If any two members of the root set have the same subject, merge them.
// This is so that messages which don't have References headers at all
// still get threaded (to the extent possible, at least.)
func (t *Threader) gatherSubjects() {
var count int
subjTable := make(map[string]*threadContainer)
for c := t.rootNode.child; c != nil; c = c.next {
threadable := c.threadable
// If there is no threadable, this is a dummy node in the root set.
// Only root set members may be dummies, and they always have at least
// two kids. Take the first kid as representative of the subject.
//
if threadable == nil {
threadable = c.child.threadable
}
subj := threadable.SimplifiedSubject()
if subj == "" {
continue
}
old := subjTable[subj]
// Add this container to the table if:
// - There is no container in the table with this subject, or
// - This one is a dummy container and the old one is not: the dummy
// one is more interesting as a root, so put it in the table instead.
// - The container in the table has a "Re:" version of this subject,
// and this container has a non-"Re:" version of this subject.
// The non-re version is the more interesting of the two.
//
if old == nil ||
(c.threadable == nil && old.threadable != nil) ||
(old.threadable != nil && old.threadable.SubjectIsReply() &&
c.threadable != nil && !c.threadable.SubjectIsReply()) {
subjTable[subj] = c
count++
}
}
// We are done if the table is empty
//
if count == 0 {
return
}
// The subj_table is now populated with one entry for each subject which
// occurs in the root set. Now iterate over the root set, and gather
// together the difference.
//
var prev, c, rest *threadContainer
prev = nil
c = t.rootNode.child
rest = c.next
for c != nil {
threadable := c.threadable
// might be a dummy -- see above
//
if threadable == nil {
threadable = c.child.threadable
}
subj := threadable.SimplifiedSubject()
// Don't thread together all subject-less messages; let them dangle.
//
if subj != "" {
old := subjTable[subj]
if old != c { // Avoid processing ourselves
// Ok, so now we have found another container in the root set with
// the same subject. There are a few possibilities:
//
// - If both are dummies, append one's children to the other, and remove
// the now-empty container.
//
// - If one container is a dummy and the other is not, make the non-dummy
// one be a child of the dummy, and a sibling of the other "real"
// messages with the same subject (the dummy's children.)
//
// - If that container is a non-dummy, and that message's subject does
// not begin with "Re:", but *this* message's subject does, then
// make this be a child of the other.
//
// - If that container is a non-dummy, and that message's subject begins
// with "Re:", but *this* message's subject does *not*, then make that
// be a child of this one -- they were mis-ordered. (This happens
// somewhat implicitly, since if there are two messages, one with Re:
// and one without, the one without will be in the hash table,
// regardless of the order in which they were seen.)
//
// - Otherwise, make a new dummy container and make both messages be a
// child of it. This catches the both-are-replies and neither-are-
// replies cases, and makes them be siblings instead of asserting a
// hierarchical relationship which might not be true.
//
// (People who reply to a message without using "Re:" and without using
// a References line will break this slightly. Those people suck.)
//
// (It has occurred to me that taking the date or message number into
// account would be one way of resolving some ambiguous cases,
// but that's not altogether straightforward either.)
// JI: You cannot rely on the clock settings being correct on a server/client that sent a message
//
// Remove the "second" message from the root set.
if prev == nil {
t.rootNode.child = c.next
} else {
prev.next = c.next
}
c.next = nil
switch {
case old.threadable == nil && c.threadable == nil:
// They're both dummies; merge them.
//
var tail *threadContainer
for tail = old.child; tail != nil && tail.next != nil; tail = tail.next {
}
tail.next = c.child
for tail = c.child; tail != nil; tail = tail.next {
tail.parent = old
}
c.child = nil
case old.threadable == nil || // old is empty, or
(c.threadable != nil &&
c.threadable.SubjectIsReply() && // c has Re, and
!old.threadable.SubjectIsReply()): // old does not.
// Make this message be a child of the other.
c.parent = old
c.next = old.child
old.child = c
default:
// Make the old and new messages be children of a new dummy container.
// We do this by creating a new container object for old->msg and
// transforming the old container into a dummy (by merely emptying it),
// so that the table still points to the one that is at depth 0
// instead of depth 1.
//
newC := &threadContainer{}
newC.threadable = old.threadable
newC.child = old.child
for tail := newC.child; tail != nil; tail = tail.next {
tail.parent = newC
}
old.threadable = nil
old.child = nil
c.parent = old
newC.parent = old
// old is now a dummy; make it have exactly two kids, c and newC.
//
old.child = c
c.next = newC
}
// we've done a merge, so keep the same `prev' next time around.
//
c = prev
}
}
prev = c
c = rest
if rest != nil {
rest = rest.next
}
}
}