package webui import ( "net/http" "net/http/httptest" "net/url" "strconv" "strings" "testing" "mailgoserver/internal/mailview" ) // TestWebmailDownloadMessageServesRawEML confirms the "Download email" action returns // the message's exact raw bytes (not the decrypted/re-parsed body — see // webmailDownloadMessage's own doc comment on why), as an attachment with a filename // derived from the subject, and that it's scoped to the requesting mailbox. func TestWebmailDownloadMessageServesRawEML(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() ownerID := createTestMailboxWithPassword(t, app, "owner@example.com", domains[0].ID, "owner-password-1!") otherID := createTestMailboxWithPassword(t, app, "other@example.com", domains[0].ID, "other-password-1!") uid := storeTestMessage(t, app, ownerID, "INBOX", "sender@example.com", "Quarterly Report", "the body") cookie := webmailLoginSession(t, app, ownerID) req := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(uid, 10)+"/download", nil) req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("download: status=%d body=%s", rec.Code, rec.Body.String()) } if ct := rec.Header().Get("Content-Type"); ct != "message/rfc822" { t.Errorf("Content-Type = %q, want message/rfc822", ct) } if cd := rec.Header().Get("Content-Disposition"); !strings.Contains(cd, `filename="Quarterly Report.eml"`) { t.Errorf("Content-Disposition = %q, want a filename derived from the subject", cd) } raw, err := app.Mailstore.FetchMessage(ownerID, uid) if err != nil { t.Fatal(err) } if rec.Body.String() != string(raw) { t.Errorf("expected the download to be the exact raw stored message") } // Scoped to the owner's own mailbox — another mailbox guessing this uid gets 404. otherCookie := webmailLoginSession(t, app, otherID) req2 := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(uid, 10)+"/download", nil) req2.AddCookie(otherCookie) rec2 := httptest.NewRecorder() mux.ServeHTTP(rec2, req2) if rec2.Code != http.StatusNotFound { t.Fatalf("expected 404 for another mailbox's message, got %d", rec2.Code) } } // TestWebmailComposeFormForwardAttachPrefill confirms opening compose with // forward_attach set prefills a "Fwd:" subject, leaves the body blank (no quoted // original — unlike a regular forward, see webmailComposeForm), and exposes the // hidden fields the compose template renders as a removable chip. func TestWebmailComposeFormForwardAttachPrefill(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() mailboxID := createTestMailboxWithPassword(t, app, "fwder@example.com", domains[0].ID, "fwder-password-1!") uid := storeTestMessage(t, app, mailboxID, "INBOX", "sender@example.com", "Original Subject", "body text") cookie := webmailLoginSession(t, app, mailboxID) req := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/compose?forward_attach="+strconv.FormatInt(uid, 10)+"&folder=INBOX", nil) req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } body := rec.Body.String() if !strings.Contains(body, `value="Fwd: Original Subject"`) { t.Errorf("expected the subject prefilled with Fwd:, got:\n%s", body) } if strings.Contains(body, "body text") { t.Errorf("expected the original body NOT quoted in (forward-as-attachment shouldn't duplicate content), got:\n%s", body) } if !strings.Contains(body, `class="forward-attach-seed" value="`+strconv.FormatInt(uid, 10)+`" data-folder="INBOX"`) { t.Errorf("expected a forward-attach-seed element for the message, got:\n%s", body) } } // TestWebmailComposeFormForwardAttachPreservesMultipleOnRetry confirms a failed send // (e.g. no recipient) re-renders the compose form with every pending forwarded // message still seeded, not just the first — the whole point of moving from three // single hidden fields to a list. func TestWebmailComposeFormForwardAttachPreservesMultipleOnRetry(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() mailboxID := createTestMailboxWithPassword(t, app, "fwder2@example.com", domains[0].ID, "fwder-password-1!") uidA := storeTestMessage(t, app, mailboxID, "INBOX", "a@example.com", "Subject A", "body a") uidB := storeTestMessage(t, app, mailboxID, "INBOX", "b@example.com", "Subject B", "body b") cookie := webmailLoginSession(t, app, mailboxID) form := url.Values{ // No "to" — deliberately triggers the "at least one recipient" validation // failure, which re-renders the form via the fail() closure. "subject": {"Fwd: multiple"}, "body_html": {"see attached"}, "forward_attach_uid": {strconv.FormatInt(uidA, 10), strconv.FormatInt(uidB, 10)}, "forward_attach_folder": {"INBOX", "INBOX"}, "forward_attach_label": {"Subject A", "Subject B"}, } req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } body := rec.Body.String() for _, uid := range []int64{uidA, uidB} { if !strings.Contains(body, `class="forward-attach-seed" value="`+strconv.FormatInt(uid, 10)+`"`) { t.Errorf("expected both forwarded messages (including uid %d) preserved on retry, got:\n%s", uid, body) } } } // TestWebmailComposeSendForwardAttach confirms the send handler reads the // forward_attach_uid/_folder hidden fields back out and attaches the original // message's exact raw bytes as a message/rfc822 attachment, and that it's re-checked // against the sender's own mailbox rather than trusted blindly (a malicious or stale // uid from another mailbox is silently ignored, matching messageAccessible's scoping — // the send itself still succeeds, just without an attachment). func TestWebmailComposeSendForwardAttach(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() domainID := domains[0].ID senderID := createTestMailboxWithPassword(t, app, "fwdsender@example.com", domainID, "sender-password-1!") recipientID := createTestMailboxWithPassword(t, app, "fwdrecip@example.com", domainID, "recipient-password-1!") originalUID := storeTestMessage(t, app, senderID, "INBOX", "someone@example.com", "Attach Me", "original body") originalRaw, err := app.Mailstore.FetchMessage(senderID, originalUID) if err != nil { t.Fatal(err) } cookie := webmailLoginSession(t, app, senderID) form := url.Values{ "to": {"fwdrecip@example.com"}, "subject": {"Fwd: Attach Me"}, "body_html": {"see attached"}, "forward_attach_uid": {strconv.FormatInt(originalUID, 10)}, "forward_attach_folder": {"INBOX"}, "forward_attach_label": {"Attach Me"}, } req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Fatalf("compose send: status=%d body=%s", rec.Code, rec.Body.String()) } recipientMsgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX") if err != nil || len(recipientMsgs) != 1 { t.Fatalf("recipient INBOX: got %d messages, err=%v", len(recipientMsgs), err) } rawReceived, err := app.Mailstore.FetchMessage(recipientID, recipientMsgs[0].ID) if err != nil { t.Fatal(err) } parsed, err := mailview.Parse(rawReceived) if err != nil { t.Fatal(err) } if len(parsed.Attachments) != 1 { t.Fatalf("expected 1 attachment (the forwarded .eml), got %d", len(parsed.Attachments)) } att := parsed.Attachments[0] if att.Filename != "Attach Me.eml" { t.Errorf("attachment filename = %q, want %q", att.Filename, "Attach Me.eml") } if att.ContentType != "message/rfc822" { t.Errorf("attachment content-type = %q, want message/rfc822", att.ContentType) } if string(att.Data) != string(originalRaw) { t.Errorf("expected the attachment to be the exact raw original message") } } // TestWebmailComposeSendMultipleForwardAttach confirms more than one forwarded // message can be attached to the same send — previously only the most recent one // survived (a single hidden field that got overwritten instead of accumulating). func TestWebmailComposeSendMultipleForwardAttach(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() domainID := domains[0].ID senderID := createTestMailboxWithPassword(t, app, "multifwdsender@example.com", domainID, "sender-password-1!") recipientID := createTestMailboxWithPassword(t, app, "multifwdrecip@example.com", domainID, "recipient-password-1!") uidA := storeTestMessage(t, app, senderID, "INBOX", "a@example.com", "First Original", "body a") uidB := storeTestMessage(t, app, senderID, "INBOX", "b@example.com", "Second Original", "body b") cookie := webmailLoginSession(t, app, senderID) form := url.Values{ "to": {"multifwdrecip@example.com"}, "subject": {"Fwd: two messages"}, "body_html": {"see attached"}, "forward_attach_uid": {strconv.FormatInt(uidA, 10), strconv.FormatInt(uidB, 10)}, "forward_attach_folder": {"INBOX", "INBOX"}, "forward_attach_label": {"First Original", "Second Original"}, } req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Fatalf("compose send: status=%d body=%s", rec.Code, rec.Body.String()) } recipientMsgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX") if err != nil || len(recipientMsgs) != 1 { t.Fatalf("recipient INBOX: got %d messages, err=%v", len(recipientMsgs), err) } rawReceived, err := app.Mailstore.FetchMessage(recipientID, recipientMsgs[0].ID) if err != nil { t.Fatal(err) } parsed, err := mailview.Parse(rawReceived) if err != nil { t.Fatal(err) } if len(parsed.Attachments) != 2 { t.Fatalf("expected 2 attachments (both forwarded .eml files), got %d", len(parsed.Attachments)) } names := map[string]bool{} for _, att := range parsed.Attachments { names[att.Filename] = true if att.ContentType != "message/rfc822" { t.Errorf("attachment %q content-type = %q, want message/rfc822", att.Filename, att.ContentType) } } if !names["First Original.eml"] || !names["Second Original.eml"] { t.Errorf("expected both First Original.eml and Second Original.eml attached, got %+v", names) } }