package webui import ( "net/http" "net/http/httptest" "os" "path/filepath" "strconv" "strings" "testing" "time" "gopkg.in/ini.v1" "mailgoserver/internal/acmecert" "mailgoserver/internal/db" "mailgoserver/internal/dkim" "mailgoserver/internal/mailstore" "mailgoserver/internal/relay" "mailgoserver/internal/toolbox" ) func newTestApp(t *testing.T) *App { t.Helper() dir := t.TempDir() dbPath := filepath.Join(dir, "test.db") database, err := db.Open(dbPath) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() }) // Seed one of everything so every page has real data to render. domainID, err := database.CreateDomain("example.com") if err != nil { t.Fatal(err) } hash, _ := db.HashPassword("testpass123") senderID, err := database.CreateSender("test@example.com", hash, domainID, true, true) if err != nil { t.Fatal(err) } _, err = database.CreateWhitelistedIP("127.0.0.1", domainID, false) if err != nil { t.Fatal(err) } dkimMgr := dkim.New(database, 1024) ok, err := dkimMgr.GenerateDKIMKeypair("example.com", "sel1", false) if err != nil || !ok { t.Fatalf("dkim gen: ok=%v err=%v", ok, err) } key, err := database.GetActiveDKIMKeyByDomainID(domainID) if err != nil || key == nil { t.Fatalf("get active dkim key: %v %v", key, err) } logID, err := database.InsertEmailLog(db.EmailLog{ MessageID: "abc123@example.com", Timestamp: time.Now(), PeerIP: "127.0.0.1", MailFrom: "test@example.com", ToAddress: "rcpt@example.org", Subject: "hi", EmailHeaders: "From: test@example.com\nTo: rcpt@example.org", MessageBody: "hello", Status: "relayed", DKIMSigned: true, Username: "test@example.com", }) if err != nil { t.Fatal(err) } if err := database.InsertEmailRecipientLog(db.EmailRecipientLog{EmailLogID: logID, Recipient: "rcpt@example.org", RecipientType: "to", Status: "success"}); err != nil { t.Fatal(err) } attFile := filepath.Join(dir, "att.txt") os.WriteFile(attFile, []byte("attachment data"), 0o644) if err := database.InsertEmailAttachment(db.EmailAttachment{EmailLogID: logID, Filename: "att.txt", ContentType: "text/plain", FilePath: attFile, Size: 15}); err != nil { t.Fatal(err) } if err := database.LogAuthAttempt("sender", "test@example.com", "127.0.0.1", true, "ok"); err != nil { t.Fatal(err) } mstore := mailstore.New(database, mailstore.GenerateDEK(), filepath.Join(dir, "mailstore")) mdek := mailstore.GenerateDEK() mwrapped, mnonce, err := mstore.WrapDEK(mdek) if err != nil { t.Fatal(err) } mboxID, err := database.CreateMailbox("inbox@example.com", hash, domainID, 5*1024*1024*1024, mwrapped, mnonce) if err != nil { t.Fatal(err) } cfg := ini.Empty() serverSec, _ := cfg.NewSection("Server") serverSec.NewKey("smtp_port", "4025") serverSec.NewKey("smtp_tls_port", "40465") serverSec.NewKey("web_http_port", "5000") serverSec.NewKey("web_https_port", "5001") serverSec.NewKey("bind_ip", "0.0.0.0") serverSec.NewKey("time_zone", "UTC") serverSec.NewKey("hostname", "mail.example.com") serverSec.NewKey("helo_hostname", "mail.example.com") serverSec.NewKey("server_banner", "") imapSec, _ := cfg.NewSection("IMAP") imapSec.NewKey("imap_port", "1143") imapSec.NewKey("imap_tls_port", "1993") authSec, _ := cfg.NewSection("Auth") authSec.NewKey("enforce_admin_mfa", "false") authSec.NewKey("enforce_mailbox_mfa", "false") rspamdSec, _ := cfg.NewSection("Rspamd") rspamdSec.NewKey("enabled", "false") rspamdSec.NewKey("url", "http://127.0.0.1:11333") rspamdSec.NewKey("reject_score", "15") dbSec, _ := cfg.NewSection("Database") dbSec.NewKey("database_url", "sqlite:///server_data/smtp_server.db") logSec, _ := cfg.NewSection("Logging") logSec.NewKey("log_level", "INFO") logSec.NewKey("hide_info_aiosmtpd", "true") relaySec, _ := cfg.NewSection("Relay") relaySec.NewKey("relay_timeout", "30") tlsSec, _ := cfg.NewSection("TLS") tlsSec.NewKey("tls_cert_file", "ssl_certs/server.crt") tlsSec.NewKey("tls_key_file", "ssl_certs/server.key") dkimSec, _ := cfg.NewSection("DKIM") dkimSec.NewKey("dkim_key_size", "2048") dkimSec.NewKey("spf_server_ip", "192.168.1.1") attSec, _ := cfg.NewSection("Attachments") attSec.NewKey("attachments_path", filepath.Join(dir, "attachments")) mailstoreSec, _ := cfg.NewSection("Mailstore") mailstoreSec.NewKey("app_password_min_length", "25") mailstoreSec.NewKey("spam_reject_score", "5") configPath := filepath.Join(dir, "settings.ini") cfg.SaveTo(configPath) acmeMgr := acmecert.New(cfg, filepath.Join(dir, "server.crt"), filepath.Join(dir, "server.key"), filepath.Join(dir, "acme"), nil, toolbox.GetLogger("test")) relayer := relay.New(database, cfg, toolbox.GetLogger("test")) appSecret, err := LoadOrCreateAppSecret(filepath.Join(dir, "app_secret.key")) if err != nil { t.Fatalf("LoadOrCreateAppSecret: %v", err) } app, err := New(database, dkimMgr, mstore, acmeMgr, relayer, cfg, configPath, toolbox.GetLogger("test"), func() bool { return true }, appSecret) if err != nil { t.Fatalf("New: %v", err) } _ = senderID _ = key _ = mboxID return app } // loginSession creates a fully-verified admin session (no MFA enrolled) and returns // its cookie, for tests that need to hit routes behind requireAuth. func loginSession(t *testing.T, app *App) *http.Cookie { t.Helper() hash, err := db.HashPassword("test-password-123!") if err != nil { t.Fatal(err) } userID, err := app.DB.CreateAdminUser("test-admin", hash, false) if err != nil { t.Fatal(err) } token, err := app.DB.CreateSession(userID, true, time.Hour) if err != nil { t.Fatal(err) } return &http.Cookie{Name: sessionCookieName, Value: token} } func TestAllPagesRender(t *testing.T) { app := newTestApp(t) mux := app.Mux() cookie := loginSession(t, app) domains, _ := app.DB.ListDomains() senders, _ := app.DB.ListSenders() mailboxes, _ := app.DB.ListMailboxes() ips, _ := app.DB.ListWhitelistedIPs() keys, _ := app.DB.ListActiveDKIMKeysWithDomain() logs, _ := app.DB.ListEmailLogsPage(0, 10) if len(domains) == 0 || len(senders) == 0 || len(mailboxes) == 0 || len(ips) == 0 || len(keys) == 0 || len(logs) == 0 { t.Fatalf("seed data missing: domains=%d senders=%d mailboxes=%d ips=%d keys=%d logs=%d", len(domains), len(senders), len(mailboxes), len(ips), len(keys), len(logs)) } pagesToCheck := []string{ "/", "/account", "/domains", "/domains/add", "/domains/" + itoa(domains[0].ID) + "/edit", "/senders", "/senders/add", "/senders/" + itoa(senders[0].ID) + "/edit", "/mailboxes", "/mailboxes/add", "/mailboxes/" + itoa(mailboxes[0].ID) + "/edit", "/mailboxes/" + itoa(mailboxes[0].ID) + "/apppasswords", "/mailboxes/" + itoa(mailboxes[0].ID) + "/aliases", "/mailboxes/" + itoa(mailboxes[0].ID) + "/lists", "/mailboxes/" + itoa(mailboxes[0].ID) + "/rules", "/ips", "/ips/add", "/ips/" + itoa(ips[0].ID) + "/edit", "/dkim", "/dkim/" + itoa(keys[0].ID) + "/edit", "/logs", "/logs?type=emails", "/logs?type=auth", "/logs?type=auth&auth_category=admin", "/logs?type=auth&auth_category=webmail", "/logs?type=auth&auth_category=mailserver", "/settings", "/letsencrypt", "/blacklist", "/msg/content/" + itoa(logs[0].ID), "/admins", "/admins/add", } for _, path := range pagesToCheck { req := httptest.NewRequest(http.MethodGet, Prefix+path, nil) req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("GET %s: status %d, body: %s", path, rec.Code, rec.Body.String()) } } } func TestUnauthenticatedRequestsRedirectToLogin(t *testing.T) { app := newTestApp(t) mux := app.Mux() req := httptest.NewRequest(http.MethodGet, Prefix+"/domains", nil) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Fatalf("expected redirect to login, got status %d", rec.Code) } loc := rec.Header().Get("Location") if !strings.HasPrefix(loc, Prefix+"/login") { t.Fatalf("expected redirect to login page, got %q", loc) } } func TestFirstLoginForcedBeforeDashboard(t *testing.T) { app := newTestApp(t) mux := app.Mux() hash, err := db.HashPassword("Password123!") if err != nil { t.Fatal(err) } userID, err := app.DB.CreateAdminUser("admin", hash, true) // must_change_password if err != nil { t.Fatal(err) } token, err := app.DB.CreateSession(userID, true, time.Hour) if err != nil { t.Fatal(err) } cookie := &http.Cookie{Name: sessionCookieName, Value: token} req := httptest.NewRequest(http.MethodGet, Prefix+"/domains", nil) req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusFound || rec.Header().Get("Location") != Prefix+"/first-login" { t.Fatalf("expected redirect to /first-login, got status %d location %q", rec.Code, rec.Header().Get("Location")) } } func itoa(id int64) string { return strconv.FormatInt(id, 10) }