mirror of https://git.sr.ht/~rjarry/aerc
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.
393 lines
9.0 KiB
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
|
|
}
|