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
}