Files
aerc-fork-mirror/lib/threadbuilder.go
Robin Jarry 73dc39c6ee treewide: replace uint32 uids with opaque strings
Add a new models.UID type (an alias to string). Replace all occurrences
of uint32 being used as message UID or thread UID with models.UID.

Update all workers to only expose models.UID values and deal with the
conversion internally. Only IMAP needs to convert these to uint32. All
other backends already use plain strings as message identifiers, in
which case no conversion is even needed.

The directory tree implementation needed to be heavily refactored in
order to accommodate thread UID not being usable as a list index.

Signed-off-by: Robin Jarry <robin@jarry.cc>
Tested-by: Inwit <inwit@sindominio.net>
Tested-by: Tim Culverhouse <tim@timculverhouse.com>
2024-08-28 12:06:01 +02:00

393 lines
9.0 KiB
Go

package lib
import (
"fmt"
"sync"
"time"
"git.sr.ht/~rjarry/aerc/lib/iterator"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
sortthread "github.com/emersion/go-imap-sortthread"
"github.com/gatherstars-com/jwz"
)
type ThreadBuilder struct {
sync.Mutex
threadBlocks map[models.UID]jwz.Threadable
threadedUids []models.UID
threadMap map[models.UID]*types.Thread
iterFactory iterator.Factory
bySubject bool
}
func NewThreadBuilder(i iterator.Factory, bySubject bool) *ThreadBuilder {
tb := &ThreadBuilder{
threadBlocks: make(map[models.UID]jwz.Threadable),
iterFactory: i,
threadMap: make(map[models.UID]*types.Thread),
bySubject: bySubject,
}
return tb
}
func (builder *ThreadBuilder) ThreadForUid(uid models.UID) (*types.Thread, error) {
builder.Lock()
defer builder.Unlock()
t, ok := builder.threadMap[uid]
var err error
if !ok {
err = fmt.Errorf("no thread found for uid '%s'", uid)
}
return t, err
}
// Uids returns the uids in threading order
func (builder *ThreadBuilder) Uids() []models.UID {
builder.Lock()
defer builder.Unlock()
if builder.threadedUids == nil {
return []models.UID{}
}
return builder.threadedUids
}
// Update updates the thread builder with a new message header
func (builder *ThreadBuilder) Update(msg *models.MessageInfo) {
builder.Lock()
defer builder.Unlock()
if msg != nil {
threadable := newThreadable(msg, builder.bySubject)
if threadable != nil {
builder.threadBlocks[msg.Uid] = threadable
}
}
}
// Threads returns a slice of threads for the given list of uids
func (builder *ThreadBuilder) Threads(uids []models.UID, inverse bool, sort bool,
) []*types.Thread {
builder.Lock()
defer builder.Unlock()
start := time.Now()
threads := builder.buildAercThreads(builder.generateStructure(uids),
uids, sort)
// sort threads according to uid ordering
builder.sortThreads(threads, uids)
// rebuild uids from threads
builder.RebuildUids(threads, inverse)
elapsed := time.Since(start)
log.Tracef("%d threads from %d uids created in %s", len(threads),
len(uids), elapsed)
return threads
}
func (builder *ThreadBuilder) generateStructure(uids []models.UID) jwz.Threadable {
jwzThreads := make([]jwz.Threadable, 0, len(builder.threadBlocks))
for _, uid := range uids {
if thr, ok := builder.threadBlocks[uid]; ok {
jwzThreads = append(jwzThreads, thr)
}
}
threader := jwz.NewThreader()
threadStructure, err := threader.ThreadSlice(jwzThreads)
if err != nil {
log.Errorf("failed slicing threads: %v", err)
}
return threadStructure
}
func (builder *ThreadBuilder) buildAercThreads(structure jwz.Threadable,
uids []models.UID, sort bool,
) []*types.Thread {
threads := make([]*types.Thread, 0, len(builder.threadBlocks))
if structure == nil {
for _, uid := range uids {
threads = append(threads, &types.Thread{Uid: uid})
}
} else {
// prepare bigger function
var bigger func(l, r *types.Thread) bool
if sort {
sortMap := make(map[models.UID]int)
for i, uid := range uids {
sortMap[uid] = i
}
bigger = func(left, right *types.Thread) bool {
if left == nil || right == nil {
return false
}
return sortMap[left.Uid] > sortMap[right.Uid]
}
} else {
bigger = func(left, right *types.Thread) bool {
if left == nil || right == nil {
return false
}
return left.Uid > right.Uid
}
}
// add uids for the unfetched messages
for _, uid := range uids {
if _, ok := builder.threadBlocks[uid]; !ok {
threads = append(threads, &types.Thread{Uid: uid})
}
}
// build thread tree
root := &types.Thread{}
builder.buildTree(structure, root, bigger, true)
// copy top-level threads to thread slice
for thread := root.FirstChild; thread != nil; thread = thread.NextSibling {
thread.Parent = nil
threads = append(threads, thread)
}
}
return threads
}
// buildTree recursively translates the jwz threads structure into aerc threads
func (builder *ThreadBuilder) buildTree(c jwz.Threadable, parent *types.Thread,
bigger func(l, r *types.Thread) bool, rootLevel bool,
) {
if c == nil || parent == nil {
return
}
for node := c; node != nil; node = node.GetNext() {
thread := builder.newThread(node, parent, node.IsDummy())
if rootLevel {
thread.NextSibling = parent.FirstChild
parent.FirstChild = thread
} else {
parent.InsertCmp(thread, bigger)
}
builder.buildTree(node.GetChild(), thread, bigger, node.IsDummy())
}
}
func (builder *ThreadBuilder) newThread(c jwz.Threadable, parent *types.Thread,
hidden bool,
) *types.Thread {
hide := 0
if hidden {
hide += 1
}
if threadable, ok := c.(*threadable); ok {
return &types.Thread{
Uid: threadable.UID(),
Parent: parent,
Hidden: hide,
}
}
return nil
}
func (builder *ThreadBuilder) sortThreads(threads []*types.Thread, orderedUids []models.UID) {
types.SortThreadsBy(threads, orderedUids)
}
// RebuildUids rebuilds the uids from the given slice of threads
func (builder *ThreadBuilder) RebuildUids(threads []*types.Thread, inverse bool) {
uids := make([]models.UID, 0, len(threads))
iterT := builder.iterFactory.NewIterator(threads)
for iterT.Next() {
var threaduids []models.UID
_ = iterT.Value().(*types.Thread).Walk(
func(t *types.Thread, level int, currentErr error) error {
stored, ok := builder.threadMap[t.Uid]
if ok {
// make this info persistent
t.Hidden = stored.Hidden
t.Deleted = stored.Deleted
}
builder.threadMap[t.Uid] = t
if t.Deleted || t.Hidden != 0 {
return nil
}
threaduids = append(threaduids, t.Uid)
return nil
})
if inverse {
for j := len(threaduids) - 1; j >= 0; j-- {
uids = append(uids, threaduids[j])
}
} else {
uids = append(uids, threaduids...)
}
}
result := make([]models.UID, 0, len(uids))
iterU := builder.iterFactory.NewIterator(uids)
for iterU.Next() {
result = append(result, iterU.Value().(models.UID))
}
builder.threadedUids = result
}
// threadable implements the jwz.threadable interface which is required for the
// jwz threading algorithm
type threadable struct {
MsgInfo *models.MessageInfo
MessageId string
Next jwz.Threadable
Parent jwz.Threadable
Child jwz.Threadable
Dummy bool
bySubject bool
}
func newThreadable(msg *models.MessageInfo, bySubject bool) *threadable {
msgid, err := msg.MsgId()
if err != nil {
return nil
}
return &threadable{
MessageId: msgid,
MsgInfo: msg,
Next: nil,
Parent: nil,
Child: nil,
Dummy: false,
bySubject: bySubject,
}
}
func (t *threadable) MessageThreadID() string {
return t.MessageId
}
func (t *threadable) MessageThreadReferences() []string {
if t.IsDummy() || t.MsgInfo == nil {
return nil
}
irp, err := t.MsgInfo.InReplyTo()
if err != nil {
irp = ""
}
refs, err := t.MsgInfo.References()
if err != nil || len(refs) == 0 {
if irp == "" {
return nil
}
refs = []string{irp}
}
return cleanRefs(t.MessageThreadID(), irp, refs)
}
// cleanRefs cleans up the references headers for threading
// 1) message-id should not be part of the references
// 2) no message-id should occur twice (avoid circularities)
// 3) in-reply-to header should not be at the beginning
func cleanRefs(m, irp string, refs []string) []string {
considered := make(map[string]interface{})
cleanRefs := make([]string, 0, len(refs))
for _, r := range refs {
if _, seen := considered[r]; r != m && !seen {
considered[r] = nil
cleanRefs = append(cleanRefs, r)
}
}
if irp != "" && len(cleanRefs) > 0 {
if cleanRefs[0] == irp {
cleanRefs = append(cleanRefs[1:], irp)
}
}
return cleanRefs
}
func (t *threadable) UID() models.UID {
if t.MsgInfo == nil {
return ""
}
return t.MsgInfo.Uid
}
func (t *threadable) Subject() string {
if !t.bySubject || t.MsgInfo == nil || t.MsgInfo.Envelope == nil {
return ""
}
return t.MsgInfo.Envelope.Subject
}
func (t *threadable) SimplifiedSubject() string {
if t.bySubject {
subject, _ := sortthread.GetBaseSubject(t.Subject())
return subject
}
return ""
}
func (t *threadable) SubjectIsReply() bool {
if t.bySubject {
_, replyOrForward := sortthread.GetBaseSubject(t.Subject())
return replyOrForward
}
return false
}
func (t *threadable) SetNext(next jwz.Threadable) {
t.Next = next
}
func (t *threadable) SetChild(kid jwz.Threadable) {
t.Child = kid
if kid != nil {
kid.SetParent(t)
}
}
func (t *threadable) SetParent(parent jwz.Threadable) {
t.Parent = parent
}
func (t *threadable) GetNext() jwz.Threadable {
return t.Next
}
func (t *threadable) GetChild() jwz.Threadable {
return t.Child
}
func (t *threadable) GetParent() jwz.Threadable {
return t.Parent
}
func (t *threadable) GetDate() time.Time {
if t.IsDummy() {
if t.GetChild() != nil {
return t.GetChild().GetDate()
}
return time.Unix(0, 0)
}
if t.MsgInfo == nil || t.MsgInfo.Envelope == nil {
return time.Unix(0, 0)
}
return t.MsgInfo.Envelope.Date
}
func (t *threadable) MakeDummy(forID string) jwz.Threadable {
return &threadable{
MessageId: forID,
Dummy: true,
}
}
func (t *threadable) IsDummy() bool {
return t.Dummy
}