feat(protocols): conversations - thread_id and in_reply_to written at delivery, list and read APIs
A reply now joins the conversation of the message it answers (Message-ID,
In-Reply-To, References), falling back to the subject for replies with no
headers. /mail/threads lists a folder as conversations and /mail/thread/{id}
returns one. Replying from the webmail flags the answered message.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ff89e9dc44
commit
67d0c8aa5a
17 files changed
+1025
-9
No files matched your search
@@ -19,10 +19,15 @@ type MessageHeaders struct {
|
||||
Subject string
|
||||
FromDisplayName string
|
||||
// FromAddress is the address in From, lowercased, or empty when it does not parse.
|
||||
FromAddress string
|
||||
MessageID string
|
||||
Snippet string
|
||||
HasAttachments bool
|
||||
FromAddress string
|
||||
MessageID string
|
||||
Snippet string
|
||||
HasAttachments bool
|
||||
// InReplyTo and References are the Message-IDs this message answers, with
|
||||
// the angle brackets removed so they compare equal to MessageID. They are
|
||||
// what ties a reply to the conversation it belongs to.
|
||||
InReplyTo string
|
||||
References []string
|
||||
}
|
||||
|
||||
// wordDecoder tolerates the charsets that turn up in real mail. An unknown
|
||||
@@ -57,6 +62,10 @@ func ExtractMessageHeaders(payload []byte) MessageHeaders {
|
||||
|
||||
out.Subject = truncateUTF8(decodeHeader(msg.Header.Get("Subject")), 998)
|
||||
out.MessageID = truncateUTF8(strings.Trim(msg.Header.Get("Message-ID"), "<>"), 998)
|
||||
if ids := messageIDsIn(msg.Header.Get("In-Reply-To")); len(ids) > 0 {
|
||||
out.InReplyTo = ids[0]
|
||||
}
|
||||
out.References = messageIDsIn(msg.Header.Get("References"))
|
||||
|
||||
if from := msg.Header.Get("From"); from != "" {
|
||||
if addr, err := mail.ParseAddress(from); err == nil {
|
||||
@@ -152,3 +161,23 @@ func truncateUTF8(s string, max int) string {
|
||||
}
|
||||
return cut
|
||||
}
|
||||
|
||||
// maxReferences bounds how much of a long chain is kept. The ids nearest the
|
||||
// end name the messages closest to this one, which is where its parent is.
|
||||
const maxReferences = 20
|
||||
|
||||
// messageIDsIn lists the <id> tokens of an In-Reply-To or References header,
|
||||
// without their brackets, in the order written.
|
||||
func messageIDsIn(header string) []string {
|
||||
var ids []string
|
||||
for _, field := range strings.Fields(header) {
|
||||
id := strings.Trim(field, "<>,;")
|
||||
if id != "" && len(id) <= 998 && strings.Contains(field, "<") {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
if len(ids) > maxReferences {
|
||||
ids = ids[len(ids)-maxReferences:]
|
||||
}
|
||||
return ids
|
||||
}
|
||||
Reference in new issue
Block a user