first commit
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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}
|
||||
}
|
||||
Reference in New Issue
Block a user