246 lines
11 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|