maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package imap
 20
 21import (
 22	"context"
 23	"crypto/tls"
 24	"errors"
 25	"fmt"
 26	"net"
 27	"strings"
 28	"sync"
 29
 30	"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)
 50
 51type Endpoint struct {
 52	addrs         []string
 53	serv          *imapserver.Server
 54	listeners     []net.Listener
 55	proxyProtocol *proxy_protocol.ProxyProtocol
 56	Store         module.Storage
 57
 58	tlsConfig   *tls.Config
 59	listenersWg sync.WaitGroup
 60
 61	saslAuth auth.SASLAuth
 62
 63	storageNormalize authz.NormalizeFunc
 64	storageMap       module.Table
 65
 66	Log log.Logger
 67}
 68
 69func 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	}
 77
 78	return endp, nil
 79}
 80
 81func (endp *Endpoint) Init(cfg *config.Map) error {
 82	var (
 83		insecureAuth bool
 84		ioDebug      bool
 85		ioErrors     bool
 86	)
 87
 88	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 err
107	}
108
109	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	}
114
115	endp.saslAuth.Log.Debug = endp.Log.Debug
116
117	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	}
125
126	endp.serv = imapserver.New(endp)
127	endp.serv.AllowInsecureAuth = insecureAuth
128	endp.serv.TLSConfig = endp.tlsConfig
129	if ioErrors {
130		endp.serv.ErrorLog = &endp.Log
131	} 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	}
138
139	if err := endp.enableExtensions(); err != nil {
140		return err
141	}
142
143	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	}
150
151	return endp.setupListeners(addresses)
152}
153
154func (endp *Endpoint) setupListeners(addresses []config.Endpoint) error {
155	for _, addr := range addresses {
156		var l net.Listener
157		var err error
158		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)
163
164		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		}
170
171		if endp.proxyProtocol != nil {
172			l = proxy_protocol.NewListener(l, endp.proxyProtocol, endp.Log)
173		}
174
175		endp.listeners = append(endp.listeners, l)
176
177		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	}
185
186	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 = true
192	}
193
194	return nil
195}
196
197func (endp *Endpoint) Name() string {
198	return "imap"
199}
200
201func (endp *Endpoint) InstanceName() string {
202	return "imap"
203}
204
205func (endp *Endpoint) Close() error {
206	for _, l := range endp.listeners {
207		l.Close()
208	}
209	if err := endp.serv.Close(); err != nil {
210		return err
211	}
212	endp.listenersWg.Wait()
213	return nil
214}
215
216func (endp *Endpoint) usernameForStorage(ctx context.Context, saslUsername string) (string, error) {
217	saslUsername, err := endp.storageNormalize(saslUsername)
218	if err != nil {
219		return "", err
220	}
221
222	if endp.storageMap == nil {
223		return saslUsername, nil
224	}
225
226	mapped, ok, err := endp.storageMap.Lookup(ctx, saslUsername)
227	if err != nil {
228		return "", err
229	}
230	if !ok {
231		return "", imapbackend.ErrInvalidCredentials
232	}
233
234	if saslUsername != mapped {
235		endp.Log.DebugMsg("using mapped username for storage", "username", saslUsername, "mapped_username", mapped)
236	}
237
238	return mapped, nil
239}
240
241func (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 err
246		}
247		endp.Log.Error("failed to determine storage account name", err, "username", username)
248		return fmt.Errorf("internal server error")
249	}
250
251	u, err := endp.Store.GetOrCreateIMAPAcct(username)
252	if err != nil {
253		return err
254	}
255	ctx := c.Context()
256	ctx.State = imap.AuthenticatedState
257	ctx.User = u
258	return nil
259}
260
261func (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.ErrInvalidCredentials
267	}
268
269	storageUsername, err := endp.usernameForStorage(context.TODO(), username)
270	if err != nil {
271		if errors.Is(err, imapbackend.ErrInvalidCredentials) {
272			return nil, err
273		}
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	}
277
278	return endp.Store.GetOrCreateIMAPAcct(storageUsername)
279}
280
281func (endp *Endpoint) I18NLevel() int {
282	be, ok := endp.Store.(i18nlevel.Backend)
283	if !ok {
284		return 0
285	}
286	return be.I18NLevel()
287}
288
289func (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	}
302
303	endp.serv.Enable(compress.NewExtension())
304	endp.serv.Enable(namespace.NewExtension())
305
306	return nil
307}
308
309func (endp *Endpoint) SupportedThreadAlgorithms() []sortthread.ThreadAlgorithm {
310	be, ok := endp.Store.(sortthread.ThreadBackend)
311	if !ok {
312		return nil
313	}
314
315	return be.SupportedThreadAlgorithms()
316}
317
318func init() {
319	module.RegisterEndpoint("imap", New)
320
321	imap.CharsetReader = message.CharsetReader
322}