fix: support SMTP LOGIN authentication
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
- 修复 QQ 邮箱、网易邮箱、Gmail 等第三方客户端可以收信但无法发信的问题。
|
||||
- SMTP 提交服务新增 `AUTH LOGIN` 认证支持,并继续兼容 `AUTH PLAIN`;两种方式均只允许在 TLS 加密连接中使用。
|
||||
- 兼容带初始用户名和标准两步用户名/密码挑战的 LOGIN 流程,适配常见手机邮箱、Apple Mail 和 Thunderbird。
|
||||
- 客户端配置保持 IMAP 993/SSL、POP3 995/SSL、SMTP 465/SSL,不增加额外服务器地址或备用配置。
|
||||
- 新增 SMTP 能力声明、LOGIN 认证、STARTTLS、隐式 TLS 和完整发信回归测试。
|
||||
@@ -4492,6 +4492,49 @@ func TestSubmissionTLSConfigReloadsCertificateFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmissionLoginAuthenticationWithAndWithoutInitialResponse(t *testing.T) {
|
||||
a := newTestApp(t)
|
||||
for _, withInitialResponse := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("initial-response-%t", withInitialResponse), func(t *testing.T) {
|
||||
session := &submissionSession{app: a}
|
||||
if mechanisms := strings.Join(session.AuthMechanisms(), " "); mechanisms != "PLAIN LOGIN" {
|
||||
t.Fatalf("submission auth mechanisms=%q", mechanisms)
|
||||
}
|
||||
server, err := session.Auth(sasl.Login)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var response []byte
|
||||
if withInitialResponse {
|
||||
response = []byte("admin@lanqin.local")
|
||||
}
|
||||
challenge, done, err := server.Next(response)
|
||||
if err != nil || done {
|
||||
t.Fatalf("initial LOGIN response err=%v done=%t", err, done)
|
||||
}
|
||||
if !withInitialResponse {
|
||||
if string(challenge) != "Username:" {
|
||||
t.Fatalf("username challenge=%q", challenge)
|
||||
}
|
||||
challenge, done, err = server.Next([]byte("admin@lanqin.local"))
|
||||
if err != nil || done {
|
||||
t.Fatalf("username response err=%v done=%t", err, done)
|
||||
}
|
||||
}
|
||||
if string(challenge) != "Password:" {
|
||||
t.Fatalf("password challenge=%q", challenge)
|
||||
}
|
||||
challenge, done, err = server.Next([]byte("ChangeMe123!"))
|
||||
if err != nil || !done || challenge != nil {
|
||||
t.Fatalf("password response challenge=%q err=%v done=%t", challenge, err, done)
|
||||
}
|
||||
if session.user == nil || session.mailbox == nil {
|
||||
t.Fatal("LOGIN authentication did not populate submission session")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmissionServersAcceptStartTLSAndImplicitTLS(t *testing.T) {
|
||||
a := newTestApp(t)
|
||||
host, port, received := startCapturingSMTP(t, 2)
|
||||
@@ -4529,7 +4572,7 @@ func TestSubmissionServersAcceptStartTLSAndImplicitTLS(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Auth(sasl.NewPlainClient("", "admin@lanqin.local", "ChangeMe123!")); err != nil {
|
||||
if err := client.Auth(sasl.NewLoginClient("admin@lanqin.local", "ChangeMe123!")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.SendMail("admin@lanqin.local", []string{"person@example.com"}, strings.NewReader(raw)); err != nil {
|
||||
|
||||
@@ -121,21 +121,59 @@ type submissionSession struct {
|
||||
}
|
||||
|
||||
func (s *submissionSession) AuthMechanisms() []string {
|
||||
return []string{sasl.Plain}
|
||||
return []string{sasl.Plain, sasl.Login}
|
||||
}
|
||||
|
||||
func (s *submissionSession) Auth(mech string) (sasl.Server, error) {
|
||||
if !strings.EqualFold(mech, sasl.Plain) {
|
||||
return nil, smtpserver.ErrAuthUnknownMechanism
|
||||
}
|
||||
return sasl.NewPlainServer(func(identity, username, password string) error {
|
||||
authenticate := func(username, password string) error {
|
||||
user, mailbox, err := s.app.authenticateSubmission(context.Background(), username, password)
|
||||
if err != nil {
|
||||
return smtpserver.ErrAuthFailed
|
||||
}
|
||||
s.user, s.mailbox = user, mailbox
|
||||
return nil
|
||||
}), nil
|
||||
}
|
||||
switch {
|
||||
case strings.EqualFold(mech, sasl.Plain):
|
||||
return sasl.NewPlainServer(func(_, username, password string) error {
|
||||
return authenticate(username, password)
|
||||
}), nil
|
||||
case strings.EqualFold(mech, sasl.Login):
|
||||
return &submissionLoginServer{authenticate: authenticate}, nil
|
||||
default:
|
||||
return nil, smtpserver.ErrAuthUnknownMechanism
|
||||
}
|
||||
}
|
||||
|
||||
type submissionLoginServer struct {
|
||||
authenticate func(username, password string) error
|
||||
username string
|
||||
step int
|
||||
}
|
||||
|
||||
func (s *submissionLoginServer) Next(response []byte) ([]byte, bool, error) {
|
||||
switch s.step {
|
||||
case 0:
|
||||
if response == nil {
|
||||
s.step = 1
|
||||
return []byte("Username:"), false, nil
|
||||
}
|
||||
s.username = string(response)
|
||||
s.step = 2
|
||||
return []byte("Password:"), false, nil
|
||||
case 1:
|
||||
s.username = string(response)
|
||||
s.step = 2
|
||||
return []byte("Password:"), false, nil
|
||||
case 2:
|
||||
if err := s.authenticate(s.username, string(response)); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
s.step = 3
|
||||
return nil, true, nil
|
||||
default:
|
||||
return nil, false, sasl.ErrUnexpectedClientResponse
|
||||
}
|
||||
}
|
||||
|
||||
func (s *submissionSession) Mail(from string, _ *smtpserver.MailOptions) error {
|
||||
|
||||
Reference in New Issue
Block a user