Files
mailgoserver/internal/webui/webmail_download_forward_test.go
T

246 lines
11 KiB
Go

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)
}
}