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

@@ -138,6 +138,13 @@ type ComposeInput struct {
// that cannot or will not render HTML still has something to show.
Text string
HTML string
// DraftUID is the autosaved draft this message grew out of, or 0 when it
// was composed without one ever being stored.
//
// Sending used to ignore it entirely, so a message that had been autosaved
// went out and its draft stayed in Drafts: a duplicate of mail already on
// its way, which no screen in the webmail could then delete.
DraftUID uint32
}
// ErrNoRecipients rejects a submission with nobody to deliver to, before it
@@ -185,9 +192,46 @@ func (uc *WebmailUseCase) Send(ctx context.Context, input ComposeInput) error {
if err := uc.fileLocalCopy(ctx, account.ID, account.EmailAddress, "Sent", payload); err != nil {
log.Printf("webmail: could not file the Sent copy for %s: %v", account.EmailAddress, err)
}
// The draft this message grew out of has been superseded by the real
// thing. Swallowed like the Sent copy above and for the same reason: the
// mail is already queued, and failing the request now would only invite a
// second copy to be sent.
if input.DraftUID != 0 {
if mailboxID, idErr := uc.mailboxID(ctx, account.EmailAddress); idErr == nil {
if err := uc.repo.Expunge(ctx, mailboxID, "Drafts", input.DraftUID); err != nil {
log.Printf("webmail: could not discard the sent draft %d: %v", input.DraftUID, err)
}
}
}
return nil
}
// Delete removes one message the way a mail client does: from any ordinary
// folder it goes to Trash, and from Trash it is really expunged.
//
// The webmail had no delete at all. Expunge existed but was reachable only
// when an autosave superseded a draft, so anything the user wanted rid of -
// most visibly the draft left behind by a sent message - stayed put.
func (uc *WebmailUseCase) Delete(ctx context.Context, address, folder string, uid uint32) error {
mailboxID, err := uc.mailboxID(ctx, address)
if err != nil {
return err
}
_, err = uc.repo.MoveToTrash(ctx, mailboxID, folder, uid)
switch {
case err == nil:
return nil
case errors.Is(err, port.ErrAlreadyInTrash), errors.Is(err, port.ErrNoTrashFolder):
// Already in Trash, or nowhere to move it to. Either way the second
// press is the one that removes it for good.
return uc.repo.Expunge(ctx, mailboxID, folder, uid)
default:
return err
}
}
// fileLocalCopy stores a message the user themselves composed into one of
// their own folders. It reuses the inbound persistence path so the copy gets a
// real IMAP UID and shows up over IMAP as well as in the webmail.
@@ -213,6 +257,7 @@ func (uc *WebmailUseCase) fileLocalCopy(
FromDisplayName: headers.FromDisplayName,
MessageIDHeader: headers.MessageID,
HasAttachments: headers.HasAttachments,
AlreadySeen: true,
}})
return err
}
@@ -261,6 +306,7 @@ func (uc *WebmailUseCase) SaveDraft(ctx context.Context, input ComposeInput, rep
FromDisplayName: headers.FromDisplayName,
MessageIDHeader: headers.MessageID,
HasAttachments: headers.HasAttachments,
AlreadySeen: true,
}})
if err != nil {
return 0, err