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

@@ -72,7 +72,7 @@ func TestParseTlsRptReport_ValidJSON(t *testing.T) {
{
"policy": {
"policy-type": "sts",
"domain": "example.test"
"policy-domain": "example.test"
},
"summary": {
"total-successful-session-count": 100,
@@ -34,7 +34,10 @@ type tlsRptJsonReport struct {
Policies []struct {
Policy struct {
PolicyType string `json:"policy-type"`
Domain string `json:"domain"`
// RFC 8460 section 4.4 names this "policy-domain". Reading it as
// "domain" parsed every real report to a blank domain, so a stored
// report could not be attributed to the domain it was about.
Domain string `json:"policy-domain"`
} `json:"policy"`
Summary struct {
TotalSuccessfulSessionCount int `json:"total-successful-session-count"`
@@ -0,0 +1,60 @@
package entity
import (
"bytes"
"compress/gzip"
"testing"
)
// This is a real report, as Google sends it daily: the gzipped JSON that
// arrives attached to a message addressed to the TLS-RPT rua.
const googleTlsRptReport = `{"organization-name":"Google Inc.",` +
`"date-range":{"start-datetime":"2026-08-19T00:00:00Z","end-datetime":"2026-08-19T23:59:59Z"},` +
`"contact-info":"smtp-tls-reporting@google.com","report-id":"2026-08-19T00:00:00Z_example.test",` +
`"policies":[{"policy":{"policy-type":"no-policy-found","policy-domain":"example.test"},` +
`"summary":{"total-successful-session-count":2,"total-failure-session-count":0}}]}`
// The field is "policy-domain" (RFC 8460 section 4.4), not "domain". Reading
// the wrong name parsed every report to a blank domain, so a stored report
// could not be attributed to the domain it was about.
func TestTlsRptReportCarriesThePolicyDomain(t *testing.T) {
report, err := ParseTlsRptReport([]byte(googleTlsRptReport))
if err != nil {
t.Fatalf("parse: %v", err)
}
if report.Domain != "example.test" {
t.Errorf("domain is %q, want example.test", report.Domain)
}
if report.OrganizationName != "Google Inc." {
t.Errorf("organization is %q", report.OrganizationName)
}
if len(report.Policies) != 1 {
t.Fatalf("want 1 policy, got %d", len(report.Policies))
}
if report.Policies[0].SuccessCount != 2 {
t.Errorf("success count is %d, want 2", report.Policies[0].SuccessCount)
}
if report.Policies[0].PolicyType != "no-policy-found" {
t.Errorf("policy type is %q", report.Policies[0].PolicyType)
}
}
// Reports arrive gzipped, which is how they land as a .json.gz attachment.
func TestTlsRptReportParsesGzippedPayload(t *testing.T) {
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
if _, err := gz.Write([]byte(googleTlsRptReport)); err != nil {
t.Fatal(err)
}
if err := gz.Close(); err != nil {
t.Fatal(err)
}
report, err := ParseTlsRptReport(buf.Bytes())
if err != nil {
t.Fatalf("parse gzipped: %v", err)
}
if report.Domain != "example.test" {
t.Errorf("domain is %q, want example.test", report.Domain)
}
}