feat: implement message deletion functionality and improve draft handling

- Added `MoveToTrash` method in `WebmailRepository` to move messages to the Trash folder instead of deleting them permanently.
- Introduced `handleDelete` method in `mailAPI` to handle message deletion requests from the client.
- Updated router to include a new route for message deletion.
- Enhanced the compose page to manage draft integrity, ensuring drafts are correctly associated with sent messages.
- Implemented UI changes to allow users to delete messages from the mail folder view.
- Added tests to verify the correct behavior of message deletion and draft handling.
- Updated database migration scripts to reconcile folder counters and ensure accurate unread counts.
- Added tests for TLS-RPT report parsing and webmail repository functionalities.
This commit is contained in:
Lucas Rayan committed 2026-08-20 15:32:14 -03:00
1 parent 4f1b9c5217
commit 880dced9e4
20 files changed
+1053 -30

No files matched your search

@@ -259,6 +259,37 @@ func (m *mailAPI) handleSend(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusAccepted, map[string]bool{"queued": true})
}
// handleDelete removes one message: to Trash from anywhere else, and for good
// from Trash itself.
func (m *mailAPI) handleDelete(w http.ResponseWriter, r *http.Request) {
session, ok := m.authenticate(w, r)
if !ok {
return
}
var body struct {
Folder string `json:"folder"`
UID uint32 `json:"uid"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "Invalid JSON")
return
}
if body.Folder == "" {
body.Folder = "inbox"
}
if body.UID == 0 {
writeError(w, http.StatusBadRequest, "INVALID_UID", "Message UID is required")
return
}
if err := m.useCase.Delete(r.Context(), session.Email, body.Folder, body.UID); err != nil {
m.fail(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
}
// handleSaveDraft stores the message being composed in the Drafts folder.
func (m *mailAPI) handleSaveDraft(w http.ResponseWriter, r *http.Request) {
session, ok := m.authenticate(w, r)
@@ -299,6 +330,9 @@ func (m *mailAPI) decodeCompose(
// previous shape - still sends a readable message rather than a blank
// one. Only used when neither text nor html is present.
LegacyBody string `json:"body"`
// DraftUID is the autosaved draft this message came from, so sending
// can clear it. Without it the draft outlived the message it became.
DraftUID uint32 `json:"draft_uid"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxComposeBytes)).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "Invalid JSON")
@@ -314,13 +348,14 @@ func (m *mailAPI) decodeCompose(
// letting the body choose From is how a webmail becomes an open relay for
// spoofed mail.
return usecase.ComposeInput{
From: sender,
To: body.To,
Cc: body.Cc,
Bcc: body.Bcc,
Subject: body.Subject,
Text: text,
HTML: body.HTML,
From: sender,
To: body.To,
Cc: body.Cc,
Bcc: body.Bcc,
Subject: body.Subject,
Text: text,
HTML: body.HTML,
DraftUID: body.DraftUID,
}, true
}
@@ -89,6 +89,7 @@ func (r *Router) registerRoutes() {
r.mux.HandleFunc("/api/v1/mail/seen", r.handleMailSeen)
r.mux.HandleFunc("/api/v1/mail/send", r.handleMailSend)
r.mux.HandleFunc("/api/v1/mail/draft", r.handleMailDraft)
r.mux.HandleFunc("/api/v1/mail/delete", r.handleMailDelete)
r.mux.HandleFunc("/api/v1/reports/dmarc", r.handleDmarcIngest)
r.mux.HandleFunc("/api/v1/reports/tlsrpt", r.handleTlsRptIngest)
}
@@ -303,6 +304,18 @@ func (r *Router) handleMailSend(w http.ResponseWriter, req *http.Request) {
}
}
// handleMailDelete removes one message. There was no route for this at all,
// so nothing the webmail displayed could be deleted from it.
func (r *Router) handleMailDelete(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost && req.Method != http.MethodDelete {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "POST or DELETE required")
return
}
if r.mailReady(w) {
r.mail.handleDelete(w, req)
}
}
func (r *Router) handleMailDraft(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "POST required")