Files
mailgoserver/internal/smtpserver/server_test.go
T
2026-08-12 12:56:22 +01:00

218 lines
5.8 KiB
Go

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)
}
}