package webui import ( "bytes" "io" "mime/multipart" "net/http" "net/http/httptest" "net/mail" "net/url" "strconv" "strings" "testing" "mailgoserver/internal/smime" ) // parseRawAsEntity is a test-only helper that reads a stored raw RFC822 message's // Content-Type header and body into a smime.Entity — enough to feed into // smime.VerifySigned/Decrypt without needing to export parseEntity from the smime // package just for tests. func parseRawAsEntity(t *testing.T, raw []byte) smime.Entity { t.Helper() msg, err := mail.ReadMessage(bytes.NewReader(raw)) if err != nil { t.Fatalf("parse raw message: %v", err) } body, err := io.ReadAll(msg.Body) if err != nil { t.Fatalf("read body: %v", err) } entity := smime.Entity{Headers: []string{"Content-Type: " + msg.Header.Get("Content-Type")}, Body: body} if cte := msg.Header.Get("Content-Transfer-Encoding"); cte != "" { entity.Headers = append(entity.Headers, "Content-Transfer-Encoding: "+cte) } return entity } // genIdentity generates and stores an S/MIME identity for a mailbox directly // (bypassing HTTP — the identity-management HTTP flow itself is covered separately // in webmail_smime_test.go), returning its ID. func genIdentity(t *testing.T, app *App, mailboxID int64, email string) int64 { t.Helper() certPEM, keyPEM, err := smime.GenerateSelfSigned(email, smime.DefaultValidity) if err != nil { t.Fatal(err) } if err := app.storeIdentity(mailboxID, certPEM, keyPEM); err != nil { t.Fatal(err) } identities, err := app.DB.ListSMIMEIdentities(mailboxID) if err != nil || len(identities) == 0 { t.Fatalf("expected the identity to be stored, err=%v", err) } return identities[0].ID // most recently created } // TestWebmailComposeSignSend confirms checking "Sign" produces a message the // recipient (or anyone) can verify as genuinely from the sender — no passphrase // involved, since S/MIME keys are stored plain in this codebase. func TestWebmailComposeSignSend(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() domainID := domains[0].ID senderID := createTestMailboxWithPassword(t, app, "signer@example.com", domainID, "signer-password-1!") recipientID := createTestMailboxWithPassword(t, app, "signee@example.com", domainID, "signee-password-1!") genIdentity(t, app, senderID, "signer@example.com") cookie := webmailLoginSession(t, app, senderID) form := url.Values{ "to": {"signee@example.com"}, "subject": {"Signed"}, "body_html": {"trust me"}, "smime_sign": {"1"}, } 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) } entity := parseRawAsEntity(t, raw) inner, signer, err := smime.VerifySigned(entity) if err != nil { t.Fatalf("VerifySigned: %v", err) } if signer.EmailAddresses[0] != "signer@example.com" { t.Fatalf("unexpected signer: %v", signer.EmailAddresses) } if !bytes.Contains(inner.Body, []byte("trust me")) { t.Fatalf("expected the body preserved inside the signed entity, got %q", inner.Body) } } // TestWebmailComposeSignedMessageWithAttachmentsShowsBody confirms a real body // message text still renders on read alongside its attachments when the message is // also S/MIME-signed — the exact combination (signed + multiple attachments + a // typed body) that was never actually exercised by any test before this one // (TestWebmailComposeSendMultipleAttachments checked attachments but not signing or // the body text; the sign-only tests never included attachments). func TestWebmailComposeSignedMessageWithAttachmentsShowsBody(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() domainID := domains[0].ID senderID := createTestMailboxWithPassword(t, app, "signedattach-sender@example.com", domainID, "sender-password-1!") recipientID := createTestMailboxWithPassword(t, app, "signedattach-recip@example.com", domainID, "recip-password-1!") genIdentity(t, app, senderID, "signedattach-sender@example.com") cookie := webmailLoginSession(t, app, senderID) var buf bytes.Buffer mw := multipart.NewWriter(&buf) mw.WriteField("to", "signedattach-recip@example.com") mw.WriteField("subject", "test message") mw.WriteField("body_html", "
this is my real message text
") mw.WriteField("smime_sign", "1") for _, name := range []string{"test.csv", "notes.md", "LICENSE"} { fw, err := mw.CreateFormFile("attachments", name) if err != nil { t.Fatal(err) } fw.Write([]byte("contents of " + name)) } 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()) } 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 body=%s", viewRec.Code, viewRec.Body.String()) } body := viewRec.Body.String() if !strings.Contains(body, "this is my real message text") { t.Fatalf("expected the message body rendered, got: %s", body) } if strings.Contains(body, "empty message body") { t.Fatal("expected the body NOT reported as empty when real text was sent") } for _, name := range []string{"test.csv", "notes.md", "LICENSE"} { if !strings.Contains(body, name) { t.Errorf("expected attachment %q listed alongside the body", name) } } } // TestWebmailComposeSignWithoutIdentityFails confirms checking "Sign" with no // identity on file fails cleanly instead of sending unsigned. func TestWebmailComposeSignWithoutIdentityFails(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() domainID := domains[0].ID senderID := createTestMailboxWithPassword(t, app, "nokey@example.com", domainID, "nokey-password-1!") recipientID := createTestMailboxWithPassword(t, app, "recip4@example.com", domainID, "recip4-password-1!") cookie := webmailLoginSession(t, app, senderID) form := url.Values{ "to": {"recip4@example.com"}, "subject": {"x"}, "body_html": {"x"}, "smime_sign": {"1"}, } 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 && rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } msgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX") if err != nil || len(msgs) != 0 { t.Fatalf("expected no message delivered, got %d (err=%v)", len(msgs), err) } }