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 }