102 lines
3.8 KiB
Go
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}
|
|
}
|