1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package imap2021import (22 "context"23 "crypto/tls"24 "errors"25 "fmt"26 "net"27 "strings"28 "sync"2930 "github.com/emersion/go-imap"31 compress "github.com/emersion/go-imap-compress"32 sortthread "github.com/emersion/go-imap-sortthread"33 imapbackend "github.com/emersion/go-imap/backend"34 imapserver "github.com/emersion/go-imap/server"35 "github.com/emersion/go-message"36 _ "github.com/emersion/go-message/charset"37 "github.com/emersion/go-sasl"38 i18nlevel "github.com/foxcpp/go-imap-i18nlevel"39 namespace "github.com/foxcpp/go-imap-namespace"40 "github.com/foxcpp/maddy/framework/config"41 modconfig "github.com/foxcpp/maddy/framework/config/module"42 tls2 "github.com/foxcpp/maddy/framework/config/tls"43 "github.com/foxcpp/maddy/framework/log"44 "github.com/foxcpp/maddy/framework/module"45 "github.com/foxcpp/maddy/internal/auth"46 "github.com/foxcpp/maddy/internal/authz"47 "github.com/foxcpp/maddy/internal/proxy_protocol"48 "github.com/foxcpp/maddy/internal/updatepipe"49)5051type Endpoint struct {52 addrs []string53 serv *imapserver.Server54 listeners []net.Listener55 proxyProtocol *proxy_protocol.ProxyProtocol56 Store module.Storage5758 tlsConfig *tls.Config59 listenersWg sync.WaitGroup6061 saslAuth auth.SASLAuth6263 storageNormalize authz.NormalizeFunc64 storageMap module.Table6566 Log log.Logger67}6869func New(modName string, addrs []string) (module.Module, error) {70 endp := &Endpoint{71 addrs: addrs,72 Log: log.Logger{Name: modName},73 saslAuth: auth.SASLAuth{74 Log: log.Logger{Name: modName + "/sasl"},75 },76 }7778 return endp, nil79}8081func (endp *Endpoint) Init(cfg *config.Map) error {82 var (83 insecureAuth bool84 ioDebug bool85 ioErrors bool86 )8788 cfg.Callback("auth", func(m *config.Map, node config.Node) error {89 return endp.saslAuth.AddProvider(m, node)90 })91 cfg.Bool("sasl_login", false, false, &endp.saslAuth.EnableLogin)92 cfg.Custom("storage", false, true, nil, modconfig.StorageDirective, &endp.Store)93 cfg.Custom("tls", true, true, nil, tls2.TLSDirective, &endp.tlsConfig)94 cfg.Custom("proxy_protocol", false, false, nil, proxy_protocol.ProxyProtocolDirective, &endp.proxyProtocol)95 cfg.Bool("insecure_auth", false, false, &insecureAuth)96 cfg.Bool("io_debug", false, false, &ioDebug)97 cfg.Bool("io_errors", false, false, &ioErrors)98 cfg.Bool("debug", true, false, &endp.Log.Debug)99 config.EnumMapped(cfg, "storage_map_normalize", false, false, authz.NormalizeFuncs, authz.NormalizeAuto,100 &endp.storageNormalize)101 modconfig.Table(cfg, "storage_map", false, false, nil, &endp.storageMap)102 config.EnumMapped(cfg, "auth_map_normalize", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,103 &endp.saslAuth.AuthNormalize)104 modconfig.Table(cfg, "auth_map", true, false, nil, &endp.saslAuth.AuthMap)105 if _, err := cfg.Process(); err != nil {106 return err107 }108109 if updBe, ok := endp.Store.(updatepipe.Backend); ok {110 if err := updBe.EnableUpdatePipe(updatepipe.ModeReplicate); err != nil {111 endp.Log.Error("failed to initialize updates pipe", err)112 }113 }114115 endp.saslAuth.Log.Debug = endp.Log.Debug116117 addresses := make([]config.Endpoint, 0, len(endp.addrs))118 for _, addr := range endp.addrs {119 saddr, err := config.ParseEndpoint(addr)120 if err != nil {121 return fmt.Errorf("imap: invalid address: %s", addr)122 }123 addresses = append(addresses, saddr)124 }125126 endp.serv = imapserver.New(endp)127 endp.serv.AllowInsecureAuth = insecureAuth128 endp.serv.TLSConfig = endp.tlsConfig129 if ioErrors {130 endp.serv.ErrorLog = &endp.Log131 } else {132 endp.serv.ErrorLog = log.Logger{Out: log.NopOutput{}}133 }134 if ioDebug {135 endp.serv.Debug = endp.Log.DebugWriter()136 endp.Log.Println("I/O debugging is on! It may leak passwords in logs, be careful!")137 }138139 if err := endp.enableExtensions(); err != nil {140 return err141 }142143 for _, mech := range endp.saslAuth.SASLMechanisms() {144 endp.serv.EnableAuth(mech, func(c imapserver.Conn) sasl.Server {145 return endp.saslAuth.CreateSASL(mech, c.Info().RemoteAddr, func(identity string, data auth.ContextData) error {146 return endp.openAccount(c, identity)147 })148 })149 }150151 return endp.setupListeners(addresses)152}153154func (endp *Endpoint) setupListeners(addresses []config.Endpoint) error {155 for _, addr := range addresses {156 var l net.Listener157 var err error158 l, err = net.Listen(addr.Network(), addr.Address())159 if err != nil {160 return fmt.Errorf("imap: %v", err)161 }162 endp.Log.Printf("listening on %v", addr)163164 if addr.IsTLS() {165 if endp.tlsConfig == nil {166 return errors.New("imap: can't bind on IMAPS endpoint without TLS configuration")167 }168 l = tls.NewListener(l, endp.tlsConfig)169 }170171 if endp.proxyProtocol != nil {172 l = proxy_protocol.NewListener(l, endp.proxyProtocol, endp.Log)173 }174175 endp.listeners = append(endp.listeners, l)176177 endp.listenersWg.Add(1)178 go func() {179 if err := endp.serv.Serve(l); err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") {180 endp.Log.Printf("imap: failed to serve %s: %s", addr, err)181 }182 endp.listenersWg.Done()183 }()184 }185186 if endp.serv.AllowInsecureAuth {187 endp.Log.Println("authentication over unencrypted connections is allowed, this is insecure configuration and should be used only for testing!")188 }189 if endp.serv.TLSConfig == nil {190 endp.Log.Println("TLS is disabled, this is insecure configuration and should be used only for testing!")191 endp.serv.AllowInsecureAuth = true192 }193194 return nil195}196197func (endp *Endpoint) Name() string {198 return "imap"199}200201func (endp *Endpoint) InstanceName() string {202 return "imap"203}204205func (endp *Endpoint) Close() error {206 for _, l := range endp.listeners {207 l.Close()208 }209 if err := endp.serv.Close(); err != nil {210 return err211 }212 endp.listenersWg.Wait()213 return nil214}215216func (endp *Endpoint) usernameForStorage(ctx context.Context, saslUsername string) (string, error) {217 saslUsername, err := endp.storageNormalize(saslUsername)218 if err != nil {219 return "", err220 }221222 if endp.storageMap == nil {223 return saslUsername, nil224 }225226 mapped, ok, err := endp.storageMap.Lookup(ctx, saslUsername)227 if err != nil {228 return "", err229 }230 if !ok {231 return "", imapbackend.ErrInvalidCredentials232 }233234 if saslUsername != mapped {235 endp.Log.DebugMsg("using mapped username for storage", "username", saslUsername, "mapped_username", mapped)236 }237238 return mapped, nil239}240241func (endp *Endpoint) openAccount(c imapserver.Conn, identity string) error {242 username, err := endp.usernameForStorage(context.TODO(), identity)243 if err != nil {244 if errors.Is(err, imapbackend.ErrInvalidCredentials) {245 return err246 }247 endp.Log.Error("failed to determine storage account name", err, "username", username)248 return fmt.Errorf("internal server error")249 }250251 u, err := endp.Store.GetOrCreateIMAPAcct(username)252 if err != nil {253 return err254 }255 ctx := c.Context()256 ctx.State = imap.AuthenticatedState257 ctx.User = u258 return nil259}260261func (endp *Endpoint) Login(connInfo *imap.ConnInfo, username, password string) (imapbackend.User, error) {262 // saslAuth handles AuthMap calling.263 err := endp.saslAuth.AuthPlain(username, password)264 if err != nil {265 endp.Log.Error("authentication failed", err, "username", username, "src_ip", connInfo.RemoteAddr)266 return nil, imapbackend.ErrInvalidCredentials267 }268269 storageUsername, err := endp.usernameForStorage(context.TODO(), username)270 if err != nil {271 if errors.Is(err, imapbackend.ErrInvalidCredentials) {272 return nil, err273 }274 endp.Log.Error("authentication failed due to an internal error", err, "username", username, "src_ip", connInfo.RemoteAddr)275 return nil, fmt.Errorf("internal server error")276 }277278 return endp.Store.GetOrCreateIMAPAcct(storageUsername)279}280281func (endp *Endpoint) I18NLevel() int {282 be, ok := endp.Store.(i18nlevel.Backend)283 if !ok {284 return 0285 }286 return be.I18NLevel()287}288289func (endp *Endpoint) enableExtensions() error {290 exts := endp.Store.IMAPExtensions()291 for _, ext := range exts {292 switch ext {293 case "I18NLEVEL=1", "I18NLEVEL=2":294 endp.serv.Enable(i18nlevel.NewExtension())295 case "SORT":296 endp.serv.Enable(sortthread.NewSortExtension())297 }298 if strings.HasPrefix(ext, "THREAD") {299 endp.serv.Enable(sortthread.NewThreadExtension())300 }301 }302303 endp.serv.Enable(compress.NewExtension())304 endp.serv.Enable(namespace.NewExtension())305306 return nil307}308309func (endp *Endpoint) SupportedThreadAlgorithms() []sortthread.ThreadAlgorithm {310 be, ok := endp.Store.(sortthread.ThreadBackend)311 if !ok {312 return nil313 }314315 return be.SupportedThreadAlgorithms()316}317318func init() {319 module.RegisterEndpoint("imap", New)320321 imap.CharsetReader = message.CharsetReader322}