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
@@ -102,6 +102,47 @@ func (m *mailAPI) handleMessages(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, messages)
|
||||
}
|
||||
|
||||
// handleThreads lists the folder as conversations, one row each.
|
||||
func (m *mailAPI) handleThreads(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := m.authenticate(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := r.URL.Query()
|
||||
folder := query.Get("folder")
|
||||
if folder == "" {
|
||||
folder = "inbox"
|
||||
}
|
||||
limit, _ := strconv.Atoi(query.Get("limit"))
|
||||
offset, _ := strconv.Atoi(query.Get("offset"))
|
||||
|
||||
threads, err := m.useCase.ListThreads(r.Context(), session.Email, folder, query.Get("q"), limit, offset)
|
||||
if err != nil {
|
||||
m.fail(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, threads)
|
||||
}
|
||||
|
||||
// handleThread returns the messages of one conversation, oldest first.
|
||||
func (m *mailAPI) handleThread(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := m.authenticate(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
threadID := strings.TrimPrefix(r.URL.Path, "/api/v1/mail/thread/")
|
||||
folder := r.URL.Query().Get("folder")
|
||||
if folder == "" {
|
||||
folder = "inbox"
|
||||
}
|
||||
messages, err := m.useCase.ThreadMessages(r.Context(), session.Email, folder, threadID)
|
||||
if err != nil {
|
||||
m.fail(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, messages)
|
||||
}
|
||||
|
||||
func (m *mailAPI) handleMessage(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := m.authenticate(w, r)
|
||||
if !ok {
|
||||
@@ -482,6 +523,8 @@ func (m *mailAPI) decodeCompose(
|
||||
// fail maps domain errors onto status codes without leaking internals.
|
||||
func (m *mailAPI) fail(w http.ResponseWriter, err error) {
|
||||
switch {
|
||||
case errors.Is(err, usecase.ErrConversationsDisabled):
|
||||
writeError(w, http.StatusNotImplemented, "CONVERSATIONS_DISABLED", "Conversations are not enabled")
|
||||
case errors.Is(err, postgres.ErrMessageNotFound), errors.Is(err, usecase.ErrNoSuchMailbox):
|
||||
writeError(w, http.StatusNotFound, "NOT_FOUND", "Message not found")
|
||||
case errors.Is(err, usecase.ErrNoRecipients):
|
||||
|
||||
@@ -98,6 +98,8 @@ func (r *Router) registerRoutes() {
|
||||
r.mux.HandleFunc("/api/v1/mail/folders", r.handleMailFolders)
|
||||
r.mux.HandleFunc("/api/v1/mail/messages", r.handleMailMessages)
|
||||
r.mux.HandleFunc("/api/v1/mail/message/", r.handleMailMessage)
|
||||
r.mux.HandleFunc("/api/v1/mail/threads", r.handleMailThreads)
|
||||
r.mux.HandleFunc("/api/v1/mail/thread/", r.handleMailThread)
|
||||
r.mux.HandleFunc("/api/v1/mail/seen", r.handleMailSeen)
|
||||
r.mux.HandleFunc("/api/v1/mail/flagged", r.handleMailFlagged)
|
||||
r.mux.HandleFunc("/api/v1/mail/send", r.handleMailSend)
|
||||
@@ -287,6 +289,18 @@ func (r *Router) handleMailFolders(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) handleMailThreads(w http.ResponseWriter, req *http.Request) {
|
||||
if r.mailReady(w) {
|
||||
r.mail.handleThreads(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) handleMailThread(w http.ResponseWriter, req *http.Request) {
|
||||
if r.mailReady(w) {
|
||||
r.mail.handleThread(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) handleMailMessages(w http.ResponseWriter, req *http.Request) {
|
||||
if r.mailReady(w) {
|
||||
r.mail.handleMessages(w, req)
|
||||
|
||||
Reference in new issue
Block a user