Files
mailgoserver/internal/webui/webmail_smime_compose_test.go
T

204 lines
7.5 KiB
Go
Raw Normal View History

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", "<p>this is my real message text</p>")
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)
}
}