You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
aerc/lib/threadbuilder.go

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
}