Skip to content

Commit d7551cf

Browse files
committed
auth/dovecot_sasl: Update go-dovecot-sasl for Dovecot 2.4 compatibility
Fixes #808
1 parent 7d94d77 commit d7551cf

File tree

6 files changed

+119
-27
lines changed

6 files changed

+119
-27
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ require (
1717
github.com/emersion/go-msgauth v0.6.8
1818
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
1919
github.com/emersion/go-smtp v0.21.3
20-
github.com/foxcpp/go-dovecot-sasl v0.0.0-20200522223722-c4699d7a24bf
20+
github.com/foxcpp/go-dovecot-sasl v0.0.0-20260303144336-f7632c6ec0ba
2121
github.com/foxcpp/go-imap-backend-tests v0.0.0-20220105184719-e80aa29a5e16
2222
github.com/foxcpp/go-imap-i18nlevel v0.0.0-20200208001533-d6ec88553005
2323
github.com/foxcpp/go-imap-mess v0.0.0-20230108134257-b7ec3a649613

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
306306
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
307307
github.com/foxcpp/go-dovecot-sasl v0.0.0-20200522223722-c4699d7a24bf h1:rmBPY5fryjp9zLQYsUmQqqgsYq7qeVfrjtr96Tf9vD8=
308308
github.com/foxcpp/go-dovecot-sasl v0.0.0-20200522223722-c4699d7a24bf/go.mod h1:5yZUmwr851vgjyAfN7OEfnrmKOh/qLA5dbGelXYsu1E=
309+
github.com/foxcpp/go-dovecot-sasl v0.0.0-20260303144336-f7632c6ec0ba h1:yxQhqX9RQCvECZKBtqwCZoKy/6CLaozDZeWH9Lvndy0=
310+
github.com/foxcpp/go-dovecot-sasl v0.0.0-20260303144336-f7632c6ec0ba/go.mod h1:5yZUmwr851vgjyAfN7OEfnrmKOh/qLA5dbGelXYsu1E=
309311
github.com/foxcpp/go-imap v1.0.0-beta.1.0.20220623182312-df940c324887 h1:qUoaaHyrRpQw85ru6VQcC6JowdhrWl7lSbI1zRX1FTM=
310312
github.com/foxcpp/go-imap v1.0.0-beta.1.0.20220623182312-df940c324887/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
311313
github.com/foxcpp/go-imap-backend-tests v0.0.0-20220105184719-e80aa29a5e16 h1:qheFPDpteiUy7Ym18R68OYenpk85UyKYGkhYTmddSBg=

internal/auth/sasl.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ type SASLAuth struct {
5454
AuthMap module.Table
5555
AuthNormalize authz.NormalizeFunc
5656

57+
ErrorMap func(err error) error
58+
5759
Plain []module.PlainAuth
5860
}
5961

@@ -132,20 +134,29 @@ type ContextData struct {
132134
}
133135

134136
// CreateSASL creates the sasl.Server instance for the corresponding mechanism.
135-
func (s *SASLAuth) CreateSASL(mech string, remoteAddr net.Addr, successCb func(identity string, data ContextData) error) sasl.Server {
137+
func (s *SASLAuth) CreateSASL(
138+
mech string, remoteAddr net.Addr,
139+
successCb func(identity string, data ContextData) error,
140+
) sasl.Server {
136141
switch mech {
137142
case sasl.Plain:
138143
return sasl.NewPlainServer(func(identity, username, password string) error {
139144
if identity == "" {
140145
identity = username
141146
}
142147
if identity != username {
148+
if s.ErrorMap != nil {
149+
return s.ErrorMap(ErrInvalidAuthCred)
150+
}
143151
return ErrInvalidAuthCred
144152
}
145153

146154
err := s.AuthPlain(username, password)
147155
if err != nil {
148156
s.Log.Error("authentication failed", err, "username", username, "src_ip", remoteAddr)
157+
if s.ErrorMap != nil {
158+
return s.ErrorMap(ErrInvalidAuthCred)
159+
}
149160
return ErrInvalidAuthCred
150161
}
151162

@@ -162,12 +173,18 @@ func (s *SASLAuth) CreateSASL(mech string, remoteAddr net.Addr, successCb func(i
162173
return sasllogin.NewLoginServer(func(username, password string) error {
163174
username, err := s.usernameForAuth(context.Background(), username)
164175
if err != nil {
176+
if s.ErrorMap != nil {
177+
return s.ErrorMap(ErrInvalidAuthCred)
178+
}
165179
return err
166180
}
167181

168182
err = s.AuthPlain(username, password)
169183
if err != nil {
170184
s.Log.Error("authentication failed", err, "username", username, "src_ip", remoteAddr)
185+
if s.ErrorMap != nil {
186+
return s.ErrorMap(ErrInvalidAuthCred)
187+
}
171188
return ErrInvalidAuthCred
172189
}
173190

internal/endpoint/smtp/session.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -171,19 +171,7 @@ func (s *Session) AuthPlain(username, password string) error {
171171

172172
failedLogins.WithLabelValues(s.endp.name).Inc()
173173

174-
if exterrors.IsTemporary(err) {
175-
return &smtp.SMTPError{
176-
Code: 454,
177-
EnhancedCode: smtp.EnhancedCode{4, 7, 0},
178-
Message: "Temporary authentication failure",
179-
}
180-
}
181-
182-
return &smtp.SMTPError{
183-
Code: 535,
184-
EnhancedCode: smtp.EnhancedCode{5, 7, 8},
185-
Message: "Invalid credentials",
186-
}
174+
return s.endp.authErrorMap(err)
187175
}
188176

189177
s.connState.AuthUser = username

internal/endpoint/smtp/smtp.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
modconfig "github.com/foxcpp/maddy/framework/config/module"
3939
tls2 "github.com/foxcpp/maddy/framework/config/tls"
4040
"github.com/foxcpp/maddy/framework/dns"
41+
"github.com/foxcpp/maddy/framework/exterrors"
4142
"github.com/foxcpp/maddy/framework/future"
4243
"github.com/foxcpp/maddy/framework/log"
4344
"github.com/foxcpp/maddy/framework/module"
@@ -285,6 +286,7 @@ func (endp *Endpoint) setConfig(cfg *config.Map) error {
285286
}
286287

287288
endp.saslAuth.Log.Debug = endp.Log.Debug
289+
endp.saslAuth.ErrorMap = endp.authErrorMap
288290

289291
// INTERNATIONALIZATION: See RFC 6531 Section 3.3.
290292
endp.serv.Domain, err = idna.ToASCII(hostname)
@@ -326,6 +328,22 @@ func (endp *Endpoint) Start() error {
326328
return nil
327329
}
328330

331+
func (endp *Endpoint) authErrorMap(err error) error {
332+
if exterrors.IsTemporary(err) {
333+
return &smtp.SMTPError{
334+
Code: 454,
335+
EnhancedCode: smtp.EnhancedCode{4, 7, 0},
336+
Message: "Temporary authentication failure",
337+
}
338+
}
339+
340+
return &smtp.SMTPError{
341+
Code: 535,
342+
EnhancedCode: smtp.EnhancedCode{5, 7, 8},
343+
Message: "Invalid credentials",
344+
}
345+
}
346+
329347
func (endp *Endpoint) setupListeners(addresses []config.Endpoint) error {
330348
for _, addr := range addresses {
331349
var l net.Listener

tests/dovecot_sasl_test.go

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
//go:build integration && (darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris)
2-
// +build integration
3-
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
42

53
/*
64
Maddy Mail Server - Composable all-in-one email server.
7-
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
5+
Copyright © 2019-2026 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
86
97
This program is free software: you can redistribute it and/or modify
108
it under the terms of the GNU General Public License as published by
@@ -26,9 +24,9 @@ package tests_test
2624

2725
import (
2826
"bufio"
27+
"bytes"
2928
"errors"
3029
"flag"
31-
"io/ioutil"
3230
"os"
3331
"os/exec"
3432
"os/user"
@@ -47,7 +45,8 @@ func init() {
4745
flag.StringVar(&DovecotExecutable, "integration.dovecot", "dovecot", "path to dovecot executable for interop tests")
4846
}
4947

50-
const dovecotConf = `base_dir = $ROOT/run/
48+
const dovecotConf = `
49+
base_dir = $ROOT/run/
5150
state_dir = $ROOT/lib/
5251
log_path = /dev/stderr
5352
ssl = no
@@ -56,12 +55,14 @@ default_internal_user = $USER
5655
default_internal_group = $GROUP
5756
default_login_user = $USER
5857
58+
auth_failure_delay = 0
59+
5960
passdb {
6061
driver = passwd-file
6162
args = $ROOT/passwd
6263
}
6364
64-
userdb {
65+
userdb file {
6566
driver = passwd-file
6667
args = $ROOT/passwd
6768
}
@@ -78,7 +79,7 @@ protocols = imap
7879
service imap-login {
7980
chroot =
8081
inet_listener imap {
81-
address = 127.0.0.1
82+
listen = 127.0.0.1
8283
port = 0
8384
}
8485
}
@@ -95,8 +96,64 @@ auth_verbose_passwords = yes
9596
mail_debug = yes
9697
`
9798

99+
const dovecotConf24 = `dovecot_config_version = 2.4.0
100+
dovecot_storage_version = 2.4.0
101+
102+
base_dir = $ROOT/run/
103+
state_dir = $ROOT/lib/
104+
mail_plugin_dir = $ROOT/lib/
105+
login_plugin_dir = $ROOT/lib/
106+
log_path = /dev/stderr
107+
ssl = no
108+
109+
default_internal_user = $USER
110+
default_internal_group = $GROUP
111+
default_login_user = $USER
112+
113+
auth_failure_delay = 0
114+
115+
passdb file {
116+
driver = passwd-file
117+
passwd_file_path = $ROOT/passwd
118+
}
119+
120+
userdb file {
121+
driver = passwd-file
122+
passwd_file_path = $ROOT/passwd
123+
}
124+
125+
service auth {
126+
unix_listener auth {
127+
mode = 0666
128+
}
129+
}
130+
131+
# Turn on debugging information, to help troubleshooting issues.
132+
auth_verbose = yes
133+
auth_debug = yes
134+
auth_debug_passwords = yes
135+
auth_verbose_passwords = yes
136+
mail_debug = yes
137+
`
138+
98139
const dovecotPasswd = `tester:{plain}123456:1000:1000::/home/user`
99140

141+
func isDovecot24(t *testing.T, dovecotExec string) bool {
142+
cmd := exec.Command(dovecotExec, "--version")
143+
var stdout bytes.Buffer
144+
cmd.Stdout = &stdout
145+
if err := cmd.Run(); err != nil {
146+
t.Fatal(err)
147+
}
148+
149+
version, _, _ := strings.Cut(stdout.String(), "-")
150+
t.Log("Dovecot version:", stdout.String())
151+
152+
parts := strings.SplitN(version, ".", 3)
153+
154+
return len(parts) >= 2 && parts[0] == "2" && parts[1] >= "4"
155+
}
156+
100157
func runDovecot(t *testing.T) (string, *exec.Cmd) {
101158
dovecotExec, err := exec.LookPath(DovecotExecutable)
102159
if err != nil {
@@ -117,15 +174,20 @@ func runDovecot(t *testing.T) (string, *exec.Cmd) {
117174
t.Fatal(err)
118175
}
119176

177+
dovecotConfTemplate := dovecotConf
178+
if isDovecot24(t, dovecotExec) {
179+
dovecotConfTemplate = dovecotConf24
180+
}
181+
120182
dovecotConf := strings.NewReplacer(
121183
"$ROOT", tempDir,
122184
"$USER", curUser.Username,
123-
"$GROUP", curGroup.Name).Replace(dovecotConf)
124-
err = ioutil.WriteFile(filepath.Join(tempDir, "dovecot.conf"), []byte(dovecotConf), os.ModePerm)
185+
"$GROUP", curGroup.Name).Replace(dovecotConfTemplate)
186+
err = os.WriteFile(filepath.Join(tempDir, "dovecot.conf"), []byte(dovecotConf), os.ModePerm)
125187
if err != nil {
126188
t.Fatal(err)
127189
}
128-
err = ioutil.WriteFile(filepath.Join(tempDir, "passwd"), []byte(dovecotPasswd), os.ModePerm)
190+
err = os.WriteFile(filepath.Join(tempDir, "passwd"), []byte(dovecotPasswd), os.ModePerm)
129191
if err != nil {
130192
t.Fatal(err)
131193
}
@@ -147,9 +209,14 @@ func runDovecot(t *testing.T) (string, *exec.Cmd) {
147209
for scnr.Scan() {
148210
line := scnr.Text()
149211

150-
// One of messages printed near completing initialization.
212+
// One of messages printed near completing initialization (Dovecot 2.3 or older)
151213
if strings.Contains(line, "starting up for imap") {
152-
time.Sleep(500*time.Millisecond)
214+
time.Sleep(500 * time.Millisecond)
215+
ready <- struct{}{}
216+
}
217+
// Dovecot 2.4+
218+
if strings.Contains(line, "starting up without any protocols") {
219+
time.Sleep(500 * time.Millisecond)
153220
ready <- struct{}{}
154221
}
155222

0 commit comments

Comments
 (0)