#24: make draft autosave replace the previous draft
The autosave I had just added appended a new message on every debounce, so a few minutes of typing would have left the Drafts folder holding a dozen copies of the same unfinished mail. Caught before deploying. The composer now returns the stored UID and sends it back as replace_uid on the next save; the use case expunges the superseded draft, but only after the replacement has committed, so a failure leaves the older draft in place rather than losing both. Expunge is a soft delete matching the expunged_at column the schema already uses, and it locates the message by mailbox as well as UID so a caller cannot delete someone else's mail by guessing a number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
707e6ae80e
commit
0fcdd15b58
6 files changed
+218
-6
No files matched your search
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -264,12 +265,13 @@ func (m *mailAPI) handleSaveDraft(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
input, ok := m.decodeCompose(w, r, session.Email)
|
||||
// The body is read once, so replace_uid has to come out of the same decode.
|
||||
input, replaceUID, ok := m.decodeDraft(w, r, session.Email)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
uid, err := m.useCase.SaveDraft(r.Context(), input)
|
||||
uid, err := m.useCase.SaveDraft(r.Context(), input, replaceUID)
|
||||
if err != nil {
|
||||
m.fail(w, err)
|
||||
return
|
||||
@@ -339,3 +341,29 @@ func (m *mailAPI) fail(w http.ResponseWriter, err error) {
|
||||
writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Request could not be processed")
|
||||
}
|
||||
}
|
||||
|
||||
// decodeDraft reads a composed message plus the draft it supersedes.
|
||||
//
|
||||
// replace_uid carries the UID of the previous autosave so the new one takes
|
||||
// its place. Without it each keystroke pause would leave another copy behind
|
||||
// and the Drafts folder would fill with the same unfinished message.
|
||||
func (m *mailAPI) decodeDraft(
|
||||
w http.ResponseWriter, r *http.Request, sender string,
|
||||
) (usecase.ComposeInput, uint32, bool) {
|
||||
raw, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxComposeBytes))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "Could not read the request")
|
||||
return usecase.ComposeInput{}, 0, false
|
||||
}
|
||||
|
||||
var meta struct {
|
||||
ReplaceUID uint32 `json:"replace_uid"`
|
||||
}
|
||||
// A missing or malformed replace_uid is not fatal: the draft is still
|
||||
// worth saving, it just will not supersede anything.
|
||||
_ = json.Unmarshal(raw, &meta)
|
||||
|
||||
r.Body = io.NopCloser(bytes.NewReader(raw))
|
||||
input, ok := m.decodeCompose(w, r, sender)
|
||||
return input, meta.ReplaceUID, ok
|
||||
}
|
||||
Reference in new issue
Block a user