aboutsummaryrefslogblamecommitdiffstats
path: root/lib/threadbuilder.go
blob: 59abd2f66577eeabbeb9e75888bb4e58be2d85c7 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11


           

              
                                        




                                             



                                                
                               

 
                                        



                                                                



                 








                                                              








                                                                                      

                                                                      

                           






                                                                                  

                                    
                                                                        



                      
                                                                               
                                                                          
                                  







                                                                
                                                                 



                              
                                                                                                         

                                                                      
                                          



                                                                          
                                          

















































                                                                                        



















                                                                                          

































                                                                               



                                         

                                           
                              

                                  
                                    
         
                                                        

 




                                                            







                                                              




                                                              
                        




                                                  



























































                                                             
package lib

import (
	"time"

	"git.sr.ht/~rjarry/aerc/logging"
	"git.sr.ht/~rjarry/aerc/models"
	"git.sr.ht/~rjarry/aerc/worker/types"
	"github.com/gatherstars-com/jwz"
)

type ThreadBuilder struct {
	threadBlocks   map[uint32]jwz.Threadable
	messageidToUid map[string]uint32
	seen           map[uint32]bool
	threadedUids   []uint32
}

func NewThreadBuilder() *ThreadBuilder {
	tb := &ThreadBuilder{
		threadBlocks:   make(map[uint32]jwz.Threadable),
		messageidToUid: make(map[string]uint32),
		seen:           make(map[uint32]bool),
	}
	return tb
}

// Uids returns the uids in threading order
func (builder *ThreadBuilder) Uids() []uint32 {
	if builder.threadedUids == nil {
		return []uint32{}
	}
	return builder.threadedUids
}

// Update updates the thread builder with a new message header
func (builder *ThreadBuilder) Update(msg *models.MessageInfo) {
	if msg != nil {
		if threadable := newThreadable(msg); threadable != nil {
			builder.messageidToUid[threadable.MessageThreadID()] = msg.Uid
			builder.threadBlocks[msg.Uid] = threadable
		}
	}
}

// Threads returns a slice of threads for the given list of uids
func (builder *ThreadBuilder) Threads(uids []uint32) []*types.Thread {
	start := time.Now()

	threads := builder.buildAercThreads(builder.generateStructure(uids), uids)

	// sort threads according to uid ordering
	builder.sortThreads(threads, uids)

	// rebuild uids from threads
	builder.RebuildUids(threads)

	elapsed := time.Since(start)
	logging.Infof("%d threads created in %s", len(threads), elapsed)

	return threads
}

func (builder *ThreadBuilder) generateStructure(uids []uint32) 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 {
		logging.Errorf("failed slicing threads: %v", err)
	}
	return threadStructure
}

func (builder *ThreadBuilder) buildAercThreads(structure jwz.Threadable, uids []uint32) []*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 {
		// fill threads with nil messages
		for _, uid := range uids {
			if _, ok := builder.threadBlocks[uid]; !ok {
				threads = append(threads, &types.Thread{Uid: uid})
			}
		}
		// append the on-the-fly created aerc threads
		root := &types.Thread{Uid: 0}
		builder.seen = make(map[uint32]bool)
		builder.buildTree(structure, root)
		for iter := root.FirstChild; iter != nil; iter = iter.NextSibling {
			iter.Parent = nil
			threads = append(threads, iter)
		}
	}
	return threads
}

// buildTree recursively translates the jwz threads structure into aerc threads
// builder.seen is used to avoid potential double-counting and should be empty
// on first call of this function
func (builder *ThreadBuilder) buildTree(treeNode jwz.Threadable, target *types.Thread) {
	if treeNode == nil {
		return
	}

	// deal with child
	uid, ok := builder.messageidToUid[treeNode.MessageThreadID()]
	if _, seen := builder.seen[uid]; ok && !seen {
		builder.seen[uid] = true
		childNode := &types.Thread{Uid: uid, Parent: target}
		target.OrderedInsert(childNode)
		builder.buildTree(treeNode.GetChild(), childNode)
	} else {
		builder.buildTree(treeNode.GetChild(), target)
	}

	// deal with siblings
	for next := treeNode.GetNext(); next != nil; next = next.GetNext() {

		uid, ok := builder.messageidToUid[next.MessageThreadID()]
		if _, seen := builder.seen[uid]; ok && !seen {
			builder.seen[uid] = true
			nn := &types.Thread{Uid: uid, Parent: target}
			target.OrderedInsert(nn)
			builder.buildTree(next.GetChild(), nn)
		} else {
			builder.buildTree(next.GetChild(), target)
		}
	}
}

func (builder *ThreadBuilder) sortThreads(threads []*types.Thread, orderedUids []uint32) {
	types.SortThreadsBy(threads, orderedUids)
}

// RebuildUids rebuilds the uids from the given slice of threads
func (builder *ThreadBuilder) RebuildUids(threads []*types.Thread) {
	uids := make([]uint32, 0, len(threads))
	for i := len(threads) - 1; i >= 0; i-- {
		threads[i].Walk(func(t *types.Thread, level int, currentErr error) error {
			uids = append(uids, t.Uid)
			return nil
		})
	}
	// copy in reverse as msgList displays backwards
	for i, j := 0, len(uids)-1; i < j; i, j = i+1, j-1 {
		uids[i], uids[j] = uids[j], uids[i]
	}
	builder.threadedUids = uids
}

// 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
}

func newThreadable(msg *models.MessageInfo) *threadable {
	msgid, err := msg.MsgId()
	if err != nil {
		return nil
	}
	return &threadable{
		MessageId: msgid,
		MsgInfo:   msg,
		Next:      nil,
		Parent:    nil,
		Child:     nil,
		Dummy:     false,
	}
}

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) Subject() string {
	// deactivate threading by subject for now
	return ""
}

func (t *threadable) SimplifiedSubject() string {
	return ""
}

func (t *threadable) SubjectIsReply() bool {
	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
}