diff options
Diffstat (limited to 'lib/mailstuff/thread.go')
-rw-r--r-- | lib/mailstuff/thread.go | 114 |
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 +} |