525 lines
21 KiB
Go
525 lines
21 KiB
Go
package webui
|
|
|
|
import (
|
|
"bytes"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"mailgoserver/internal/mailview"
|
|
)
|
|
|
|
// TestWebmailComposeSendLocalDelivery confirms a composed message reaches another
|
|
// local mailbox's INBOX with the right content, and a copy lands in the sender's own
|
|
// Sent folder — the core send/receive round trip.
|
|
func TestWebmailComposeSendLocalDelivery(t *testing.T) {
|
|
app := newTestApp(t)
|
|
mux := app.Mux()
|
|
domains, _ := app.DB.ListDomains()
|
|
domainID := domains[0].ID
|
|
|
|
senderID := createTestMailboxWithPassword(t, app, "sender@example.com", domainID, "sender-password-1!")
|
|
recipientID := createTestMailboxWithPassword(t, app, "recipient@example.com", domainID, "recipient-password-1!")
|
|
cookie := webmailLoginSession(t, app, senderID)
|
|
|
|
form := url.Values{
|
|
"to": {"recipient@example.com"}, "subject": {"Hello there"}, "body_html": {"This is the message body."},
|
|
}
|
|
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 {
|
|
t.Fatal(err)
|
|
}
|
|
if len(recipientMsgs) != 1 {
|
|
t.Fatalf("expected 1 message in recipient's INBOX, got %d", len(recipientMsgs))
|
|
}
|
|
if recipientMsgs[0].CachedSubject != "Hello there" {
|
|
t.Errorf("recipient subject = %q", recipientMsgs[0].CachedSubject)
|
|
}
|
|
|
|
senderSent, err := app.DB.ListMessagesInFolder(senderID, "Sent")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(senderSent) != 1 {
|
|
t.Fatalf("expected 1 message in sender's Sent folder, got %d", len(senderSent))
|
|
}
|
|
|
|
// Recipient can actually read it via the message view.
|
|
recipientCookie := webmailLoginSession(t, app, recipientID)
|
|
viewReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(recipientMsgs[0].ID, 10), nil)
|
|
viewReq.AddCookie(recipientCookie)
|
|
viewRec := httptest.NewRecorder()
|
|
mux.ServeHTTP(viewRec, viewReq)
|
|
if viewRec.Code != http.StatusOK {
|
|
t.Fatalf("view message: status=%d", viewRec.Code)
|
|
}
|
|
if !strings.Contains(viewRec.Body.String(), "This is the message body.") {
|
|
t.Error("expected the message body in the rendered view")
|
|
}
|
|
|
|
// It's also recorded in the admin email log for visibility.
|
|
logs, _ := app.DB.ListEmailLogsPage(0, 10)
|
|
found := false
|
|
for _, l := range logs {
|
|
if l.Subject == "Hello there" && l.MailFrom == "sender@example.com" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected the webmail send to show up in the admin email log")
|
|
}
|
|
}
|
|
|
|
// TestWebmailMessageHTMLBodyIsSanitized confirms a malicious HTML body (e.g. from a
|
|
// received message, not something webmail's own plain-text compose can produce) never
|
|
// reaches the page unsanitized — this is the actual stored-XSS defense.
|
|
func TestWebmailMessageHTMLBodyIsSanitized(t *testing.T) {
|
|
app := newTestApp(t)
|
|
mux := app.Mux()
|
|
domains, _ := app.DB.ListDomains()
|
|
mailboxID := createTestMailboxWithPassword(t, app, "victim@example.com", domains[0].ID, "victim-password-1!")
|
|
|
|
raw := "From: attacker@evil.example\r\nTo: victim@example.com\r\nSubject: gotcha\r\n" +
|
|
"Content-Type: text/html\r\n\r\n" +
|
|
`<p>hello</p><script>alert(document.cookie)</script><img src=x onerror="alert(1)">`
|
|
uid, err := app.Mailstore.StoreMessage(mailboxID, "INBOX", []byte(raw), "x@example.com", "attacker@evil.example", "gotcha")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cookie := webmailLoginSession(t, app, mailboxID)
|
|
req := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(uid, 10), nil)
|
|
req.AddCookie(cookie)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("view message: status=%d", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
// The page legitimately has its own <script> tags (Bootstrap/toast JS) — check
|
|
// for the actual injected payload surviving, not the literal substring "<script>".
|
|
if strings.Contains(body, "alert(document.cookie)") || strings.Contains(body, "onerror=") {
|
|
t.Error("HTML body was not sanitized — script/event-handler survived into the rendered page")
|
|
}
|
|
if !strings.Contains(body, "<p>hello</p>") {
|
|
t.Error("expected the safe formatting to survive sanitization")
|
|
}
|
|
}
|
|
|
|
// TestWebmailAttachmentRoundTrip confirms a file attached during compose survives
|
|
// send, local delivery, and download with identical bytes.
|
|
func TestWebmailAttachmentRoundTrip(t *testing.T) {
|
|
app := newTestApp(t)
|
|
mux := app.Mux()
|
|
domains, _ := app.DB.ListDomains()
|
|
domainID := domains[0].ID
|
|
|
|
senderID := createTestMailboxWithPassword(t, app, "sender2@example.com", domainID, "sender-password-1!")
|
|
recipientID := createTestMailboxWithPassword(t, app, "recipient2@example.com", domainID, "recipient-password-1!")
|
|
cookie := webmailLoginSession(t, app, senderID)
|
|
|
|
var buf bytes.Buffer
|
|
mw := multipart.NewWriter(&buf)
|
|
mw.WriteField("to", "recipient2@example.com")
|
|
mw.WriteField("subject", "With attachment")
|
|
mw.WriteField("body_html", "see attached")
|
|
fw, err := mw.CreateFormFile("attachments", "notes.txt")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fw.Write([]byte("attachment file contents"))
|
|
mw.Close()
|
|
|
|
req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", &buf)
|
|
req.Header.Set("Content-Type", mw.FormDataContentType())
|
|
req.AddCookie(cookie)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusFound {
|
|
t.Fatalf("compose send with attachment: 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)
|
|
}
|
|
uid := recipientMsgs[0].ID
|
|
|
|
recipientCookie := webmailLoginSession(t, app, recipientID)
|
|
dlReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(uid, 10)+"/attachment/0", nil)
|
|
dlReq.AddCookie(recipientCookie)
|
|
dlRec := httptest.NewRecorder()
|
|
mux.ServeHTTP(dlRec, dlReq)
|
|
if dlRec.Code != http.StatusOK {
|
|
t.Fatalf("download attachment: status=%d", dlRec.Code)
|
|
}
|
|
if got := dlRec.Body.String(); got != "attachment file contents" {
|
|
t.Errorf("downloaded attachment = %q, want original content", got)
|
|
}
|
|
}
|
|
|
|
// TestWebmailComposeSendMultipleAttachments confirms sending 2+ files under the
|
|
// "attachments" field name — the shape the accumulating dropzone picker in
|
|
// webmail_compose.html produces — delivers all of them, not just the first. The
|
|
// server-side loop over r.MultipartForm.File["attachments"] already handled this
|
|
// before the dropzone UI existed; this closes the test-coverage gap that let "only
|
|
// one attachment works" go unnoticed (it was a frontend picker limitation, not a
|
|
// server one — see the accumulating DataTransfer-backed picker in Milestone C).
|
|
func TestWebmailComposeSendMultipleAttachments(t *testing.T) {
|
|
app := newTestApp(t)
|
|
mux := app.Mux()
|
|
domains, _ := app.DB.ListDomains()
|
|
domainID := domains[0].ID
|
|
|
|
senderID := createTestMailboxWithPassword(t, app, "multisender@example.com", domainID, "sender-password-1!")
|
|
recipientID := createTestMailboxWithPassword(t, app, "multirecip@example.com", domainID, "recipient-password-1!")
|
|
cookie := webmailLoginSession(t, app, senderID)
|
|
|
|
var buf bytes.Buffer
|
|
mw := multipart.NewWriter(&buf)
|
|
mw.WriteField("to", "multirecip@example.com")
|
|
mw.WriteField("subject", "Three files")
|
|
mw.WriteField("body_html", "see attached")
|
|
for i, name := range []string{"a.txt", "b.txt", "c.txt"} {
|
|
fw, err := mw.CreateFormFile("attachments", name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fw.Write([]byte("contents of file " + strconv.Itoa(i)))
|
|
}
|
|
mw.Close()
|
|
|
|
req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", &buf)
|
|
req.Header.Set("Content-Type", mw.FormDataContentType())
|
|
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)
|
|
}
|
|
|
|
recipientCookie := webmailLoginSession(t, app, recipientID)
|
|
viewReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(recipientMsgs[0].ID, 10), nil)
|
|
viewReq.AddCookie(recipientCookie)
|
|
viewRec := httptest.NewRecorder()
|
|
mux.ServeHTTP(viewRec, viewReq)
|
|
if viewRec.Code != http.StatusOK {
|
|
t.Fatalf("view message: status=%d", viewRec.Code)
|
|
}
|
|
for _, name := range []string{"a.txt", "b.txt", "c.txt"} {
|
|
if !strings.Contains(viewRec.Body.String(), name) {
|
|
t.Errorf("expected attachment %q listed on the message page", name)
|
|
}
|
|
}
|
|
if !strings.Contains(viewRec.Body.String(), "see attached") {
|
|
t.Errorf("expected the message body rendered alongside the attachments, got: %s", viewRec.Body.String())
|
|
}
|
|
|
|
for i, name := range []string{"a.txt", "b.txt", "c.txt"} {
|
|
dlReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(recipientMsgs[0].ID, 10)+"/attachment/"+strconv.Itoa(i), nil)
|
|
dlReq.AddCookie(recipientCookie)
|
|
dlRec := httptest.NewRecorder()
|
|
mux.ServeHTTP(dlRec, dlReq)
|
|
if dlRec.Code != http.StatusOK {
|
|
t.Fatalf("download attachment %d (%s): status=%d", i, name, dlRec.Code)
|
|
}
|
|
want := "contents of file " + strconv.Itoa(i)
|
|
if got := dlRec.Body.String(); got != want {
|
|
t.Errorf("attachment %d content = %q, want %q", i, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWebmailComposeRejectsEmptySend confirms the server-side guard (not just the
|
|
// client-side JS, which a test can't exercise) refuses to send a message with no
|
|
// subject, and separately one with no body and no attachments.
|
|
func TestWebmailComposeRejectsEmptySend(t *testing.T) {
|
|
app := newTestApp(t)
|
|
mux := app.Mux()
|
|
domains, _ := app.DB.ListDomains()
|
|
domainID := domains[0].ID
|
|
|
|
senderID := createTestMailboxWithPassword(t, app, "emptysender@example.com", domainID, "sender-password-1!")
|
|
recipientID := createTestMailboxWithPassword(t, app, "emptyrecip@example.com", domainID, "recipient-password-1!")
|
|
cookie := webmailLoginSession(t, app, senderID)
|
|
|
|
send := func(t *testing.T, form url.Values) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
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)
|
|
return rec
|
|
}
|
|
|
|
// A rejected send redisplays the compose form (status 200) with the posted
|
|
// content still filled in, rather than redirecting to a blank one.
|
|
noSubject := url.Values{"to": {"emptyrecip@example.com"}, "subject": {""}, "body_html": {"hello"}}
|
|
rec := send(t, noSubject)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("no-subject send: status=%d", rec.Code)
|
|
}
|
|
|
|
noBody := url.Values{"to": {"emptyrecip@example.com"}, "subject": {"hi"}, "body_html": {" "}}
|
|
rec = send(t, noBody)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("no-body send: status=%d", rec.Code)
|
|
}
|
|
|
|
msgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX")
|
|
if err != nil || len(msgs) != 0 {
|
|
t.Fatalf("expected neither blank send delivered, got %d messages (err=%v)", len(msgs), err)
|
|
}
|
|
}
|
|
|
|
// TestWebmailComposeHTMLBodyRoundTrip confirms an HTML compose body (what Quill
|
|
// submits) survives send/delivery as a proper multipart/alternative — both the
|
|
// formatted HTML and a derived plain-text fallback reach the recipient — and renders
|
|
// with its formatting intact on the message page.
|
|
func TestWebmailComposeHTMLBodyRoundTrip(t *testing.T) {
|
|
app := newTestApp(t)
|
|
mux := app.Mux()
|
|
domains, _ := app.DB.ListDomains()
|
|
domainID := domains[0].ID
|
|
|
|
senderID := createTestMailboxWithPassword(t, app, "htmlsender@example.com", domainID, "sender-password-1!")
|
|
recipientID := createTestMailboxWithPassword(t, app, "htmlrecip@example.com", domainID, "recipient-password-1!")
|
|
cookie := webmailLoginSession(t, app, senderID)
|
|
|
|
form := url.Values{
|
|
"to": {"htmlrecip@example.com"}, "subject": {"Formatted"},
|
|
"body_html": {"<p><strong>Bold</strong> and <em>italic</em> text.</p>"},
|
|
}
|
|
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())
|
|
}
|
|
|
|
msgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX")
|
|
if err != nil || len(msgs) != 1 {
|
|
t.Fatalf("expected 1 message, got %d (err=%v)", len(msgs), err)
|
|
}
|
|
raw, err := app.Mailstore.FetchMessage(recipientID, msgs[0].ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parsed, err := mailview.Parse(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(parsed.HTMLBody, "<strong>Bold</strong>") {
|
|
t.Errorf("expected the HTML body to survive formatting, got %q", parsed.HTMLBody)
|
|
}
|
|
if !strings.Contains(parsed.TextBody, "Bold and italic text") {
|
|
t.Errorf("expected a plain-text fallback part derived from the HTML, got %q", parsed.TextBody)
|
|
}
|
|
|
|
recipientCookie := webmailLoginSession(t, app, recipientID)
|
|
viewReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(msgs[0].ID, 10), nil)
|
|
viewReq.AddCookie(recipientCookie)
|
|
viewRec := httptest.NewRecorder()
|
|
mux.ServeHTTP(viewRec, viewReq)
|
|
if viewRec.Code != http.StatusOK {
|
|
t.Fatalf("view: status=%d", viewRec.Code)
|
|
}
|
|
if !strings.Contains(viewRec.Body.String(), "<strong>Bold</strong>") {
|
|
t.Error("expected the formatted HTML rendered on the message page")
|
|
}
|
|
}
|
|
|
|
// TestWebmailComposePastedImageSurvivesRoundTrip confirms a pasted screenshot
|
|
// (Quill's clipboard module embeds it as a base64 data: URI <img>) survives compose,
|
|
// delivery, and sanitize-on-view unchanged — this is what makes "paste a screenshot"
|
|
// actually work end to end, not just accepted at compose time.
|
|
func TestWebmailComposePastedImageSurvivesRoundTrip(t *testing.T) {
|
|
app := newTestApp(t)
|
|
mux := app.Mux()
|
|
domains, _ := app.DB.ListDomains()
|
|
domainID := domains[0].ID
|
|
|
|
senderID := createTestMailboxWithPassword(t, app, "imgsender@example.com", domainID, "sender-password-1!")
|
|
recipientID := createTestMailboxWithPassword(t, app, "imgrecip@example.com", domainID, "recipient-password-1!")
|
|
cookie := webmailLoginSession(t, app, senderID)
|
|
|
|
imgSrc := "data:image/png;base64,aGVsbG8="
|
|
form := url.Values{
|
|
"to": {"imgrecip@example.com"}, "subject": {"Screenshot"},
|
|
"body_html": {`<p>See attached: <img src="` + imgSrc + `"></p>`},
|
|
}
|
|
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())
|
|
}
|
|
|
|
msgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX")
|
|
if err != nil || len(msgs) != 1 {
|
|
t.Fatalf("expected 1 message, got %d (err=%v)", len(msgs), err)
|
|
}
|
|
|
|
recipientCookie := webmailLoginSession(t, app, recipientID)
|
|
viewReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(msgs[0].ID, 10), nil)
|
|
viewReq.AddCookie(recipientCookie)
|
|
viewRec := httptest.NewRecorder()
|
|
mux.ServeHTTP(viewRec, viewReq)
|
|
if viewRec.Code != http.StatusOK {
|
|
t.Fatalf("view: status=%d", viewRec.Code)
|
|
}
|
|
if !strings.Contains(viewRec.Body.String(), imgSrc) {
|
|
t.Errorf("expected the pasted base64 image to survive sanitize-on-view, got body: %s", viewRec.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestWebmailComposeReplyPrefill confirms replying prefills To/Subject/quoted body
|
|
// from the original message.
|
|
func TestWebmailComposeReplyPrefill(t *testing.T) {
|
|
app := newTestApp(t)
|
|
mux := app.Mux()
|
|
domains, _ := app.DB.ListDomains()
|
|
mailboxID := createTestMailboxWithPassword(t, app, "replier@example.com", domains[0].ID, "replier-password-1!")
|
|
|
|
raw := "From: original@example.com\r\nTo: replier@example.com\r\nSubject: Original subject\r\nMessage-Id: <orig123@example.com>\r\n\r\noriginal body text"
|
|
uid, err := app.Mailstore.StoreMessage(mailboxID, "INBOX", []byte(raw), "orig123@example.com", "original@example.com", "Original subject")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cookie := webmailLoginSession(t, app, mailboxID)
|
|
req := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/compose?reply="+strconv.FormatInt(uid, 10)+"&folder=INBOX", nil)
|
|
req.AddCookie(cookie)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("compose reply prefill: status=%d", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
if !strings.Contains(body, "original@example.com") {
|
|
t.Error("expected the To field prefilled with the original sender")
|
|
}
|
|
if !strings.Contains(body, "Re: Original subject") {
|
|
t.Error("expected the subject prefilled with a Re: prefix")
|
|
}
|
|
if !strings.Contains(body, "original body text") {
|
|
t.Error("expected the original body quoted")
|
|
}
|
|
// The new message goes ABOVE the quoted original, not mixed into or after it —
|
|
// composeCursorHome (webmail_compose.go) prepends an empty line the cursor gets
|
|
// placed in (see webmail_compose.html's seed-loading JS), so the seed must start
|
|
// with that empty paragraph before the "On ... wrote:" quote line.
|
|
seedIdx := strings.Index(body, `id="body_html_seed"`)
|
|
quoteIdx := strings.Index(body, "wrote:")
|
|
emptyLineIdx := strings.Index(body, "<p><br></p>")
|
|
if seedIdx < 0 || quoteIdx < 0 || emptyLineIdx < 0 || !(seedIdx < emptyLineIdx && emptyLineIdx < quoteIdx) {
|
|
t.Errorf("expected an empty line before the quoted original (for the new message to go above it), got: %s", body)
|
|
}
|
|
}
|
|
|
|
// TestWebmailMoveAndDeleteMessage confirms moving a message changes its folder, and
|
|
// deleting from a non-Trash folder moves to Trash first, requiring a second delete to
|
|
// actually remove it.
|
|
func TestWebmailMoveAndDeleteMessage(t *testing.T) {
|
|
app := newTestApp(t)
|
|
mux := app.Mux()
|
|
domains, _ := app.DB.ListDomains()
|
|
mailboxID := createTestMailboxWithPassword(t, app, "organizer@example.com", domains[0].ID, "organizer-password-1!")
|
|
cookie := webmailLoginSession(t, app, mailboxID)
|
|
|
|
raw := "From: a@example.com\r\nTo: organizer@example.com\r\nSubject: sort me\r\n\r\nbody"
|
|
uid, err := app.Mailstore.StoreMessage(mailboxID, "INBOX", []byte(raw), "m1@example.com", "a@example.com", "sort me")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
uidStr := strconv.FormatInt(uid, 10)
|
|
|
|
moveReq := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/INBOX/"+uidStr+"/move", strings.NewReader("target_folder=Work"))
|
|
moveReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
moveReq.AddCookie(cookie)
|
|
moveRec := httptest.NewRecorder()
|
|
mux.ServeHTTP(moveRec, moveReq)
|
|
if moveRec.Code != http.StatusFound {
|
|
t.Fatalf("move: status=%d", moveRec.Code)
|
|
}
|
|
moved, err := app.DB.GetMessageByUID(mailboxID, uid)
|
|
if err != nil || moved == nil || moved.Folder != "Work" {
|
|
t.Fatalf("expected message moved to Work, got %+v (err=%v)", moved, err)
|
|
}
|
|
|
|
delReq := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/Work/"+uidStr+"/delete", nil)
|
|
delReq.AddCookie(cookie)
|
|
delRec := httptest.NewRecorder()
|
|
mux.ServeHTTP(delRec, delReq)
|
|
if delRec.Code != http.StatusFound {
|
|
t.Fatalf("delete (to trash): status=%d", delRec.Code)
|
|
}
|
|
trashed, err := app.DB.GetMessageByUID(mailboxID, uid)
|
|
if err != nil || trashed == nil || trashed.Folder != "Trash" {
|
|
t.Fatalf("expected message moved to Trash, got %+v (err=%v)", trashed, err)
|
|
}
|
|
|
|
del2Req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/Trash/"+uidStr+"/delete", nil)
|
|
del2Req.AddCookie(cookie)
|
|
del2Rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(del2Rec, del2Req)
|
|
if del2Rec.Code != http.StatusFound {
|
|
t.Fatalf("delete (permanent): status=%d", del2Rec.Code)
|
|
}
|
|
gone, err := app.DB.GetMessageByUID(mailboxID, uid)
|
|
if err != nil || gone != nil {
|
|
t.Fatalf("expected message permanently gone, got %+v (err=%v)", gone, err)
|
|
}
|
|
}
|
|
|
|
// TestWebmailMessageAccessControlAcrossMailboxes confirms one mailbox owner can't
|
|
// view another mailbox's message by guessing its UID, even in a folder name they
|
|
// both happen to have.
|
|
func TestWebmailMessageAccessControlAcrossMailboxes(t *testing.T) {
|
|
app := newTestApp(t)
|
|
mux := app.Mux()
|
|
domains, _ := app.DB.ListDomains()
|
|
domainID := domains[0].ID
|
|
|
|
ownerID := createTestMailboxWithPassword(t, app, "owner3@example.com", domainID, "owner-password-1!")
|
|
attackerID := createTestMailboxWithPassword(t, app, "attacker3@example.com", domainID, "attacker-password-1!")
|
|
|
|
raw := "From: a@example.com\r\nTo: owner3@example.com\r\nSubject: private\r\n\r\nsecret body"
|
|
uid, err := app.Mailstore.StoreMessage(ownerID, "INBOX", []byte(raw), "m2@example.com", "a@example.com", "private")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
attackerCookie := webmailLoginSession(t, app, attackerID)
|
|
req := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(uid, 10), nil)
|
|
req.AddCookie(attackerCookie)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404 for another mailbox's message, got %d", rec.Code)
|
|
}
|
|
}
|