250 lines
8.3 KiB
Go
250 lines
8.3 KiB
Go
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/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"))
|
|
app, err := New(database, dkimMgr, mstore, acmeMgr, cfg, configPath, toolbox.GetLogger("test"), func() bool { return true })
|
|
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",
|
|
"/settings",
|
|
"/letsencrypt",
|
|
"/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)
|
|
}
|