summaryrefslogtreecommitdiff
path: root/lib/mailstuff/thread.go
diff options
context:
space:
mode:
Diffstat (limited to 'lib/mailstuff/thread.go')
-rw-r--r--lib/mailstuff/thread.go114
1 files changed, 114 insertions, 0 deletions
diff --git a/lib/mailstuff/thread.go b/lib/mailstuff/thread.go
new file mode 100644
index 0000000..2cdf9a4
--- /dev/null
+++ b/lib/mailstuff/thread.go
@@ -0,0 +1,114 @@
+package mailstuff
+
+import (
+ "fmt"
+ "net/mail"
+ "regexp"
+ "strings"
+)
+
+type Set[T comparable] map[T]struct{}
+
+func (s Set[T]) Insert(val T) {
+ s[val] = struct{}{}
+}
+
+func mapHas[K comparable, V any](m map[K]V, k K) bool {
+ _, ok := m[k]
+ return ok
+}
+
+func (s Set[T]) Has(val T) bool {
+ return mapHas(s, val)
+}
+
+func (s Set[T]) PickOne() T {
+ for v := range s {
+ return v
+ }
+ var zero T
+ return zero
+}
+
+type MessageID string
+
+type ThreadedMessage struct {
+ *mail.Message
+ Parent *ThreadedMessage
+ Children Set[*ThreadedMessage]
+}
+
+var reReplyID = regexp.MustCompile("<[^> \t\r\n]+>")
+
+func rfc2822parse(msg *mail.Message) *jwzMessage {
+ // TODO: This is bad, and needs a real implementation.
+ ret := &jwzMessage{
+ Subject: msg.Header.Get("Subject"),
+ ID: jwzID(msg.Header.Get("Message-ID")),
+ }
+ refIDs := strings.Fields(msg.Header.Get("References"))
+ strings.Fields(msg.Header.Get("References"))
+ if replyID := reReplyID.FindString(msg.Header.Get("In-Reply-To")); replyID != "" {
+ refIDs = append(refIDs, replyID)
+ }
+ ret.References = make([]jwzID, len(refIDs))
+ for i := range refIDs {
+ ret.References[i] = jwzID(refIDs[i])
+ }
+ return ret
+}
+
+func ThreadMessages(msgs []*mail.Message) (Set[*ThreadedMessage], map[MessageID]*ThreadedMessage) {
+ jwzMsgs := make(map[jwzID]*jwzMessage, len(msgs))
+ retMsgs := make(map[jwzID]*ThreadedMessage, len(msgs))
+ bogusCnt := 0
+ for _, msg := range msgs {
+ jwzMsg := rfc2822parse(msg)
+
+ // RFC 5256:
+ //
+ // If a message does not contain a Message-ID header
+ // line, or the Message-ID header line does not
+ // contain a valid Message ID, then assign a unique
+ // Message ID to this message.
+ //
+ // If two or more messages have the same Message ID,
+ // then only use that Message ID in the first (lowest
+ // sequence number) message, and assign a unique
+ // Message ID to each of the subsequent messages with
+ // a duplicate of that Message ID.
+ for jwzMsg.ID == "" || mapHas(jwzMsgs, jwzMsg.ID) {
+ jwzMsg.ID = jwzID(fmt.Sprintf("bogus.%d", bogusCnt))
+ bogusCnt++
+ }
+
+ jwzMsgs[jwzMsg.ID] = jwzMsg
+ retMsgs[jwzMsg.ID] = &ThreadedMessage{
+ Message: msg,
+ }
+ }
+
+ jwzThreads := jwzThreadMessages(jwzMsgs)
+
+ var convertMessage func(*jwzContainer) *ThreadedMessage
+ convertMessage = func(in *jwzContainer) *ThreadedMessage {
+ var out *ThreadedMessage
+ if in.Message == nil {
+ out = new(ThreadedMessage)
+ } else {
+ out = retMsgs[in.Message.ID]
+ }
+ out.Children = make(Set[*ThreadedMessage], len(in.Children))
+ for inChild := range in.Children {
+ outChild := convertMessage(inChild)
+ out.Children.Insert(outChild)
+ outChild.Parent = out
+ }
+ return out
+ }
+ retThreads := make(Set[*ThreadedMessage], len(jwzThreads))
+ for inThread := range jwzThreads {
+ retThreads.Insert(convertMessage(inThread))
+ }
+ return retThreads, retMsgs
+}