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

102 lines
3.8 KiB
Go

package smtpserver
import (
"fmt"
"time"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"mailgoserver/internal/db"
)
// loginServer implements the LOGIN SASL mechanism server-side (go-sasl only ships the
// client half), mirroring the state machine aiosmtpd's built-in LOGIN handler drives:
// ask for username (unless an initial response already supplied it), then password.
type loginServer struct {
state int // 0: need username, 1: need password
username string
verify func(username, password string) error
}
func (s *loginServer) Next(response []byte) (challenge []byte, done bool, err error) {
switch s.state {
case 0:
if response == nil {
return []byte("Username:"), false, nil
}
s.username = string(response)
s.state = 1
return []byte("Password:"), false, nil
case 1:
password := string(response)
if err := s.verify(s.username, password); err != nil {
return nil, false, err
}
return nil, true, nil
default:
return nil, false, fmt.Errorf("unexpected LOGIN state")
}
}
// AuthMechanisms mirrors CustomSMTP._get_auth_methods's effective mechanism set
// (aiosmtpd's default LOGIN/PLAIN) once auth is allowed at all — the TLS-required gate
// itself is handled by go-smtp's own AllowInsecureAuth/isTLS check per listener.
func (s *Session) AuthMechanisms() []string {
return []string{sasl.Login, sasl.Plain}
}
// Auth mirrors EnhancedCombinedAuthenticator.__call__ for the LOGIN/PLAIN case (the
// only mechanisms advertised): credentials are always present by the time verify runs,
// so the "no auth_data supplied" fallback branch in the Python version is unreachable
// here and isn't replicated.
func (s *Session) Auth(mech string) (sasl.Server, error) {
switch mech {
case sasl.Login:
return &loginServer{verify: s.authenticate}, nil
case sasl.Plain:
return sasl.NewPlainServer(func(identity, username, password string) error {
return s.authenticate(username, password)
}), nil
default:
return nil, smtp.ErrAuthUnknownMechanism
}
}
// authenticate mirrors EnhancedAuthenticator.__call__: verifies credentials, logs an
// AuthLog row either way, and on any failure returns a *smtp.SMTPError carrying the
// exact Python response code/message, arming the connection to close right after that
// response is flushed — mirroring CustomSMTP.smtp_AUTH's transport.close() override.
func (s *Session) authenticate(username, password string) error {
sender, err := s.backend.DB.GetSenderByEmail(username)
if err != nil {
s.backend.Logger.Error("Authentication error: %v", err)
_ = s.backend.DB.LogAuthAttempt("sender", username, s.peerIP, false, fmt.Sprintf("Authentication error: %v", err))
return s.failAuth(451, "Internal server error")
}
if sender == nil || !db.CheckPassword(password, sender.PasswordHash) {
_ = s.backend.DB.LogAuthAttempt("sender", username, s.peerIP, false, fmt.Sprintf("Invalid credentials for %s", username))
return s.failAuth(535, "Authentication failed")
}
s.authenticatedSender = sender
s.authType = "sender"
s.username = username
_ = s.backend.DB.LogAuthAttempt("sender", username, s.peerIP, true, "Successful sender authentication")
return nil
}
// failAuth builds the SMTPError for a failed AUTH attempt and closes the connection
// shortly after go-smtp writes this response, mirroring CustomSMTP.smtp_AUTH's
// transport.close() override. go-smtp writes the response synchronously right after
// this error is returned, so a short delay comfortably outlasts that write without
// needing to intercept the raw connection (which would break TLS detection on the
// implicit-TLS listener — see server.go).
func (s *Session) failAuth(code int, message string) error {
conn := s.conn
go func() {
time.Sleep(100 * time.Millisecond)
conn.Close()
}()
return &smtp.SMTPError{Code: code, EnhancedCode: smtp.NoEnhancedCode, Message: message}
}