package smtpserver import ( "net" "net/smtp" "os" "strings" "testing" "time" "gopkg.in/ini.v1" "mailgoserver/internal/db" "mailgoserver/internal/dkim" "mailgoserver/internal/relay" "mailgoserver/internal/toolbox" ) func newTestBackend(t *testing.T) *Backend { t.Helper() f, err := os.CreateTemp("", "smtp-test-*.db") if err != nil { t.Fatal(err) } f.Close() t.Cleanup(func() { os.Remove(f.Name()) }) database, err := db.Open(f.Name()) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() }) if _, err := database.Exec(`INSERT INTO esrv_domains (domain_name, is_active, is_verified) VALUES ('example.com', 1, 1)`); err != nil { t.Fatal(err) } hash, err := db.HashPassword("testpass123") if err != nil { t.Fatal(err) } if _, err := database.Exec(`INSERT INTO esrv_senders (email, password_hash, domain_id, is_active) VALUES (?, ?, 1, 1)`, "test@example.com", hash); err != nil { t.Fatal(err) } if _, err := database.Exec(`INSERT INTO esrv_whitelisted_ips (ip_address, domain_id, is_active) VALUES ('127.0.0.1', 1, 1)`); err != nil { t.Fatal(err) } cfg := ini.Empty() logger := toolbox.GetLogger("test") return &Backend{ DB: database, DKIM: dkim.New(database, 1024), Relay: relay.New(database, cfg, logger), Cfg: cfg, Logger: logger, HeloHostname: "mail.example.com", AttachmentsBasePath: t.TempDir(), } } func startTestServer(t *testing.T, backend *Backend) string { t.Helper() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } srv := NewPlainServer(backend, l.Addr().String(), "mail.example.com") go srv.Serve(l) t.Cleanup(func() { srv.Close() }) return l.Addr().String() } func TestAuthSuccessAndSenderAuthorization(t *testing.T) { backend := newTestBackend(t) addr := startTestServer(t, backend) c, err := smtp.Dial(addr) if err != nil { t.Fatal(err) } defer c.Close() if err := c.Auth(smtp.PlainAuth("", "test@example.com", "testpass123", "127.0.0.1")); err != nil { t.Fatalf("expected auth success, got: %v", err) } if err := c.Mail("test@example.com"); err != nil { t.Fatalf("expected MAIL FROM as own address to succeed, got: %v", err) } if err := c.Rcpt("someone@elsewhere.example"); err != nil { t.Fatalf("expected RCPT to accept any address, got: %v", err) } } func TestAuthFailureClosesConnection(t *testing.T) { backend := newTestBackend(t) addr := startTestServer(t, backend) c, err := smtp.Dial(addr) if err != nil { t.Fatal(err) } defer c.Close() err = c.Auth(smtp.PlainAuth("", "test@example.com", "wrongpassword", "127.0.0.1")) if err == nil { t.Fatal("expected auth failure") } if !strings.Contains(err.Error(), "535") { t.Fatalf("expected 535 response, got: %v", err) } // The server should close the connection shortly after — a subsequent command // must fail rather than succeed. time.Sleep(300 * time.Millisecond) if err := c.Mail("test@example.com"); err == nil { t.Fatal("expected connection to have been closed after failed AUTH") } } func TestIPWhitelistFallbackWithoutAuth(t *testing.T) { backend := newTestBackend(t) addr := startTestServer(t, backend) c, err := smtp.Dial(addr) if err != nil { t.Fatal(err) } defer c.Close() // No AUTH at all: MAIL FROM a domain whitelisted for our (loopback) peer IP. if err := c.Mail("anyone@example.com"); err != nil { t.Fatalf("expected IP-whitelist fallback to authorize, got: %v", err) } if err := c.Rcpt("rcpt@elsewhere.example"); err != nil { t.Fatalf("expected RCPT to accept, got: %v", err) } } func TestMailFromRejectedForUnauthorizedDomain(t *testing.T) { backend := newTestBackend(t) addr := startTestServer(t, backend) c, err := smtp.Dial(addr) if err != nil { t.Fatal(err) } defer c.Close() err = c.Mail("nobody@not-whitelisted.example") if err == nil { t.Fatal("expected MAIL FROM to be rejected for a non-whitelisted, non-authenticated domain") } if !strings.Contains(err.Error(), "550") { t.Fatalf("expected 550 response, got: %v", err) } } func TestUnverifiedDomainCannotSend(t *testing.T) { backend := newTestBackend(t) domainID, err := backend.DB.CreateDomain("unverified.example") if err != nil { t.Fatal(err) } hash, _ := db.HashPassword("testpass123") if _, err := backend.DB.CreateSender("sender@unverified.example", hash, domainID, false, false); err != nil { t.Fatal(err) } addr := startTestServer(t, backend) c, err := smtp.Dial(addr) if err != nil { t.Fatal(err) } defer c.Close() if err := c.Auth(smtp.PlainAuth("", "sender@unverified.example", "testpass123", "127.0.0.1")); err != nil { t.Fatalf("auth: %v", err) } err = c.Mail("sender@unverified.example") if err == nil { t.Fatal("expected MAIL FROM to be rejected for an unverified domain, even for an authenticated sender") } if !strings.Contains(err.Error(), "550") || !strings.Contains(err.Error(), "verif") { t.Fatalf("expected a 550 mentioning verification, got: %v", err) } // Now verify the domain directly (bypassing DNS) and confirm sending is unblocked. if err := backend.DB.SetDomainVerified(domainID, true); err != nil { t.Fatal(err) } if err := c.Mail("sender@unverified.example"); err != nil { t.Fatalf("expected MAIL FROM to succeed once domain is verified, got: %v", err) } } func TestSenderCannotSpoofOtherAddress(t *testing.T) { backend := newTestBackend(t) addr := startTestServer(t, backend) c, err := smtp.Dial(addr) if err != nil { t.Fatal(err) } defer c.Close() if err := c.Auth(smtp.PlainAuth("", "test@example.com", "testpass123", "127.0.0.1")); err != nil { t.Fatalf("auth: %v", err) } err = c.Mail("someoneelse@example.com") if err == nil { t.Fatal("expected MAIL FROM spoofing another address to be rejected (can_send_as_domain is false)") } if !strings.Contains(err.Error(), "550") { t.Fatalf("expected 550 response, got: %v", err) } }