mirror of
https://git.sr.ht/~rjarry/aerc
synced 2026-03-02 18:23:33 +01:00
Add a new models.UID type (an alias to string). Replace all occurrences of uint32 being used as message UID or thread UID with models.UID. Update all workers to only expose models.UID values and deal with the conversion internally. Only IMAP needs to convert these to uint32. All other backends already use plain strings as message identifiers, in which case no conversion is even needed. The directory tree implementation needed to be heavily refactored in order to accommodate thread UID not being usable as a list index. Signed-off-by: Robin Jarry <robin@jarry.cc> Tested-by: Inwit <inwit@sindominio.net> Tested-by: Tim Culverhouse <tim@timculverhouse.com>
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
|
|
}
|