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

@@ -0,0 +1,82 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
/**
* The composer's draft handling, and the delete action the mail list was
* missing entirely.
*
* Every check here is a defect the user hit: a message sent from the composer
* left its autosaved draft behind, that draft was empty because the autosave
* never saw the body, and no screen offered any way to remove it.
*/
const read = (path: string) => readFileSync(resolve(process.cwd(), path), "utf8");
const code = (path: string) =>
read(path)
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/\{\/\*[\s\S]*?\*\/\}/g, "")
.replace(/^\s*\/\/.*$/gm, "");
const COMPOSE = "src/app/(user)/user/compose/page.tsx";
const MAIL_PAGE = "src/app/(user)/user/mail/[folder]/page.tsx";
describe("the draft a sent message leaves behind", () => {
// Sending posted to /mail/send without ever naming the draft it came from,
// so the draft stayed in Drafts as a duplicate of mail already on its way.
it("tells the server which draft the sent message came from", () => {
const compose = code(COMPOSE);
expect(compose).toMatch(/draft_uid:\s*draftUidRef\.current/);
});
it("posts the draft UID on the send request, not only on the autosave", () => {
const compose = code(COMPOSE);
const sendCall = compose.slice(compose.indexOf('"/api/v1/mail/send"'));
expect(sendCall.slice(0, 600)).toContain("draft_uid");
});
});
describe("what the autosave actually stores", () => {
// The body lived in a contenteditable read through a ref, and the debounce
// depended only on the header fields. Typing a message therefore never
// re-triggered a save, so the stored draft held the recipient and nothing
// else - the empty draft the user could see and could not delete.
it("re-runs when the body changes, not only the recipient and subject", () => {
const compose = code(COMPOSE);
const effect = compose.slice(compose.indexOf("const timer = setTimeout(() => void saveDraft()"));
const deps = effect.slice(effect.indexOf("}, ["), effect.indexOf("]);") + 3);
expect(deps).toContain("body");
});
it("keeps the body in state so a change can be observed at all", () => {
const compose = code(COMPOSE);
expect(compose).toMatch(/useState.*\n?.*body|const \[body, setBody\]/);
expect(compose).toContain("setBody");
});
it("does not save a draft that has no content anywhere", () => {
const compose = code(COMPOSE);
expect(compose).toMatch(/if \(!to && !subject && !body/);
});
});
describe("deleting a message", () => {
// There was no delete anywhere in the webmail: no button, and no route
// behind one. Anything the user wanted rid of stayed forever.
it("offers a delete action on the message list", () => {
const page = code(MAIL_PAGE);
expect(page).toContain("/api/v1/mail/delete");
});
it("refreshes the counters after deleting, so the badges follow", () => {
const page = code(MAIL_PAGE);
const del = page.slice(page.indexOf('"/api/v1/mail/delete"'));
expect(del.slice(0, 900)).toContain("notifyMailStateChanged");
});
it("removes the deleted row from the list without a full reload", () => {
const page = code(MAIL_PAGE);
const del = page.slice(page.indexOf("const deleteMessage"));
expect(del.slice(0, 1200)).toMatch(/setMessages\(/);
});
});