2026-08-14 13:04:55 +01:00
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 ))
}
2026-08-15 12:35:44 +01:00
if isUnread ( senderSent [ 0 ]. Flags ) {
t . Error ( "expected the Sent copy to be marked read, not unread" )
}
if ! isUnread ( recipientMsgs [ 0 ]. Flags ) {
t . Error ( "expected the recipient's INBOX copy to still be unread" )
}
2026-08-14 13:04:55 +01:00
// 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" )
}
2026-08-15 12:35:44 +01:00
// It's also recorded in the admin email log for visibility — but never with the
// real body content (privacy default; the Sent folder above already keeps the
// real, encrypted-at-rest copy).
2026-08-14 13:04:55 +01:00
logs , _ := app . DB . ListEmailLogsPage ( 0 , 10 )
found := false
for _ , l := range logs {
if l . Subject == "Hello there" && l . MailFrom == "sender@example.com" {
found = true
2026-08-15 12:35:44 +01:00
if strings . Contains ( l . MessageBody , "This is the message body." ) {
t . Errorf ( "expected the real body not to be logged, got %q" , l . MessageBody )
}
2026-08-14 13:04:55 +01:00
}
}
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 )
}
}