maddy/internal/endpoint/imap/imap.go
fox.cpp 78e4600a17
Upgrade all dependencies
1. go-smtp is replaced by a fork that reverts StartTLS removal.
2. SASL LOGIN is no longer supported by upstream go-sasl, readded disabled by default.
3. Updated endpoint code to match new go-smtp authentication interfaces.
4. certmagic repo had some renames
5. Minimum Go version increased to 1.23 to match dependencies.
2025-01-24 21:29:48 +03:00

347 lines
9.6 KiB
Go

/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package imap
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"strings"
"sync"
"github.com/emersion/go-imap"
compress "github.com/emersion/go-imap-compress"
sortthread "github.com/emersion/go-imap-sortthread"
imapbackend "github.com/emersion/go-imap/backend"
imapserver "github.com/emersion/go-imap/server"
"github.com/emersion/go-message"
_ "github.com/emersion/go-message/charset"
"github.com/emersion/go-sasl"
i18nlevel "github.com/foxcpp/go-imap-i18nlevel"
namespace "github.com/foxcpp/go-imap-namespace"
"github.com/foxcpp/maddy/framework/config"
modconfig "github.com/foxcpp/maddy/framework/config/module"
tls2 "github.com/foxcpp/maddy/framework/config/tls"
"github.com/foxcpp/maddy/framework/log"
"github.com/foxcpp/maddy/framework/module"
"github.com/foxcpp/maddy/internal/auth"
"github.com/foxcpp/maddy/internal/authz"
"github.com/foxcpp/maddy/internal/proxy_protocol"
"github.com/foxcpp/maddy/internal/updatepipe"
)
type Endpoint struct {
addrs []string
serv *imapserver.Server
listeners []net.Listener
proxyProtocol *proxy_protocol.ProxyProtocol
Store module.Storage
tlsConfig *tls.Config
listenersWg sync.WaitGroup
saslAuth auth.SASLAuth
storageNormalize authz.NormalizeFunc
storageMap module.Table
authNormalize authz.NormalizeFunc
authMap module.Table
Log log.Logger
}
func New(modName string, addrs []string) (module.Module, error) {
endp := &Endpoint{
addrs: addrs,
Log: log.Logger{Name: modName},
saslAuth: auth.SASLAuth{
Log: log.Logger{Name: modName + "/sasl"},
},
}
return endp, nil
}
func (endp *Endpoint) Init(cfg *config.Map) error {
var (
insecureAuth bool
ioDebug bool
ioErrors bool
)
cfg.Callback("auth", func(m *config.Map, node config.Node) error {
return endp.saslAuth.AddProvider(m, node)
})
cfg.Bool("sasl_login", false, false, &endp.saslAuth.EnableLogin)
cfg.Custom("storage", false, true, nil, modconfig.StorageDirective, &endp.Store)
cfg.Custom("tls", true, true, nil, tls2.TLSDirective, &endp.tlsConfig)
cfg.Custom("proxy_protocol", false, false, nil, proxy_protocol.ProxyProtocolDirective, &endp.proxyProtocol)
cfg.Bool("insecure_auth", false, false, &insecureAuth)
cfg.Bool("io_debug", false, false, &ioDebug)
cfg.Bool("io_errors", false, false, &ioErrors)
cfg.Bool("debug", true, false, &endp.Log.Debug)
config.EnumMapped(cfg, "storage_map_normalize", false, false, authz.NormalizeFuncs, authz.NormalizeAuto,
&endp.storageNormalize)
modconfig.Table(cfg, "storage_map", false, false, nil, &endp.storageMap)
config.EnumMapped(cfg, "auth_map_normalize", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,
&endp.authNormalize)
modconfig.Table(cfg, "auth_map", true, false, nil, &endp.authMap)
if _, err := cfg.Process(); err != nil {
return err
}
if updBe, ok := endp.Store.(updatepipe.Backend); ok {
if err := updBe.EnableUpdatePipe(updatepipe.ModeReplicate); err != nil {
endp.Log.Error("failed to initialize updates pipe", err)
}
}
addresses := make([]config.Endpoint, 0, len(endp.addrs))
for _, addr := range endp.addrs {
saddr, err := config.ParseEndpoint(addr)
if err != nil {
return fmt.Errorf("imap: invalid address: %s", addr)
}
addresses = append(addresses, saddr)
}
endp.serv = imapserver.New(endp)
endp.serv.AllowInsecureAuth = insecureAuth
endp.serv.TLSConfig = endp.tlsConfig
if ioErrors {
endp.serv.ErrorLog = &endp.Log
} else {
endp.serv.ErrorLog = log.Logger{Out: log.NopOutput{}}
}
if ioDebug {
endp.serv.Debug = endp.Log.DebugWriter()
endp.Log.Println("I/O debugging is on! It may leak passwords in logs, be careful!")
}
if err := endp.enableExtensions(); err != nil {
return err
}
endp.saslAuth.AuthNormalize = endp.authNormalize
endp.saslAuth.AuthMap = endp.authMap
for _, mech := range endp.saslAuth.SASLMechanisms() {
mech := mech
endp.serv.EnableAuth(mech, func(c imapserver.Conn) sasl.Server {
return endp.saslAuth.CreateSASL(mech, c.Info().RemoteAddr, func(identity string, data auth.ContextData) error {
return endp.openAccount(c, identity)
})
})
}
return endp.setupListeners(addresses)
}
func (endp *Endpoint) setupListeners(addresses []config.Endpoint) error {
for _, addr := range addresses {
var l net.Listener
var err error
l, err = net.Listen(addr.Network(), addr.Address())
if err != nil {
return fmt.Errorf("imap: %v", err)
}
endp.Log.Printf("listening on %v", addr)
if addr.IsTLS() {
if endp.tlsConfig == nil {
return errors.New("imap: can't bind on IMAPS endpoint without TLS configuration")
}
l = tls.NewListener(l, endp.tlsConfig)
}
if endp.proxyProtocol != nil {
l = proxy_protocol.NewListener(l, endp.proxyProtocol, endp.Log)
}
endp.listeners = append(endp.listeners, l)
endp.listenersWg.Add(1)
addr := addr
go func() {
if err := endp.serv.Serve(l); err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") {
endp.Log.Printf("imap: failed to serve %s: %s", addr, err)
}
endp.listenersWg.Done()
}()
}
if endp.serv.AllowInsecureAuth {
endp.Log.Println("authentication over unencrypted connections is allowed, this is insecure configuration and should be used only for testing!")
}
if endp.serv.TLSConfig == nil {
endp.Log.Println("TLS is disabled, this is insecure configuration and should be used only for testing!")
endp.serv.AllowInsecureAuth = true
}
return nil
}
func (endp *Endpoint) Name() string {
return "imap"
}
func (endp *Endpoint) InstanceName() string {
return "imap"
}
func (endp *Endpoint) Close() error {
for _, l := range endp.listeners {
l.Close()
}
if err := endp.serv.Close(); err != nil {
return err
}
endp.listenersWg.Wait()
return nil
}
func (endp *Endpoint) usernameForAuth(ctx context.Context, saslUsername string) (string, error) {
saslUsername, err := endp.authNormalize(saslUsername)
if err != nil {
return "", err
}
if endp.authMap == nil {
return saslUsername, nil
}
mapped, ok, err := endp.authMap.Lookup(ctx, saslUsername)
if err != nil {
return "", err
}
if !ok {
return "", imapbackend.ErrInvalidCredentials
}
return mapped, nil
}
func (endp *Endpoint) usernameForStorage(ctx context.Context, saslUsername string) (string, error) {
saslUsername, err := endp.storageNormalize(saslUsername)
if err != nil {
return "", err
}
if endp.storageMap == nil {
return saslUsername, nil
}
mapped, ok, err := endp.storageMap.Lookup(ctx, saslUsername)
if err != nil {
return "", err
}
if !ok {
return "", imapbackend.ErrInvalidCredentials
}
if saslUsername != mapped {
endp.Log.DebugMsg("using mapped username for storage", "username", saslUsername, "mapped_username", mapped)
}
return mapped, nil
}
func (endp *Endpoint) openAccount(c imapserver.Conn, identity string) error {
username, err := endp.usernameForStorage(context.TODO(), identity)
if err != nil {
if errors.Is(err, imapbackend.ErrInvalidCredentials) {
return err
}
endp.Log.Error("failed to determine storage account name", err, "username", username)
return fmt.Errorf("internal server error")
}
u, err := endp.Store.GetOrCreateIMAPAcct(username)
if err != nil {
return err
}
ctx := c.Context()
ctx.State = imap.AuthenticatedState
ctx.User = u
return nil
}
func (endp *Endpoint) Login(connInfo *imap.ConnInfo, username, password string) (imapbackend.User, error) {
// saslAuth handles AuthMap calling.
err := endp.saslAuth.AuthPlain(username, password)
if err != nil {
endp.Log.Error("authentication failed", err, "username", username, "src_ip", connInfo.RemoteAddr)
return nil, imapbackend.ErrInvalidCredentials
}
storageUsername, err := endp.usernameForStorage(context.TODO(), username)
if err != nil {
if errors.Is(err, imapbackend.ErrInvalidCredentials) {
return nil, err
}
endp.Log.Error("authentication failed due to an internal error", err, "username", username, "src_ip", connInfo.RemoteAddr)
return nil, fmt.Errorf("internal server error")
}
return endp.Store.GetOrCreateIMAPAcct(storageUsername)
}
func (endp *Endpoint) I18NLevel() int {
be, ok := endp.Store.(i18nlevel.Backend)
if !ok {
return 0
}
return be.I18NLevel()
}
func (endp *Endpoint) enableExtensions() error {
exts := endp.Store.IMAPExtensions()
for _, ext := range exts {
switch ext {
case "I18NLEVEL=1", "I18NLEVEL=2":
endp.serv.Enable(i18nlevel.NewExtension())
case "SORT":
endp.serv.Enable(sortthread.NewSortExtension())
}
if strings.HasPrefix(ext, "THREAD") {
endp.serv.Enable(sortthread.NewThreadExtension())
}
}
endp.serv.Enable(compress.NewExtension())
endp.serv.Enable(namespace.NewExtension())
return nil
}
func (endp *Endpoint) SupportedThreadAlgorithms() []sortthread.ThreadAlgorithm {
be, ok := endp.Store.(sortthread.ThreadBackend)
if !ok {
return nil
}
return be.SupportedThreadAlgorithms()
}
func init() {
module.RegisterEndpoint("imap", New)
imap.CharsetReader = message.CharsetReader
}