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 dovecotsasl
 20
 21import (
 22	"fmt"
 23	"net"
 24
 25	"github.com/emersion/go-sasl"
 26	dovecotsasl "github.com/foxcpp/go-dovecot-sasl"
 27	"github.com/foxcpp/maddy/framework/config"
 28	"github.com/foxcpp/maddy/framework/exterrors"
 29	"github.com/foxcpp/maddy/framework/log"
 30	"github.com/foxcpp/maddy/framework/module"
 31	"github.com/foxcpp/maddy/internal/auth"
 32)
 33
 34type Auth struct {
 35	instName       string
 36	serverEndpoint string
 37	log            log.Logger
 38
 39	network string
 40	addr    string
 41
 42	mechanisms map[string]dovecotsasl.Mechanism
 43}
 44
 45const modName = "dovecot_sasl"
 46
 47func New(_, instName string, _, inlineArgs []string) (module.Module, error) {
 48	a := &Auth{
 49		instName: instName,
 50		log:      log.Logger{Name: modName, Debug: log.DefaultLogger.Debug},
 51	}
 52
 53	switch len(inlineArgs) {
 54	case 0:
 55	case 1:
 56		a.serverEndpoint = inlineArgs[0]
 57	default:
 58		return nil, fmt.Errorf("%s: one or none arguments needed", modName)
 59	}
 60
 61	return a, nil
 62}
 63
 64func (a *Auth) Name() string {
 65	return modName
 66}
 67
 68func (a *Auth) InstanceName() string {
 69	return a.instName
 70}
 71
 72func (a *Auth) getConn() (*dovecotsasl.Client, error) {
 73	// TODO: Connection pooling
 74	conn, err := net.Dial(a.network, a.addr)
 75	if err != nil {
 76		return nil, fmt.Errorf("%s: unable to contact server: %v", modName, err)
 77	}
 78
 79	cl, err := dovecotsasl.NewClient(conn)
 80	if err != nil {
 81		return nil, fmt.Errorf("%s: unable to contact server: %v", modName, err)
 82	}
 83
 84	return cl, nil
 85}
 86
 87func (a *Auth) returnConn(cl *dovecotsasl.Client) {
 88	cl.Close()
 89}
 90
 91func (a *Auth) Init(cfg *config.Map) error {
 92	cfg.String("endpoint", false, false, a.serverEndpoint, &a.serverEndpoint)
 93	if _, err := cfg.Process(); err != nil {
 94		return err
 95	}
 96	if a.serverEndpoint == "" {
 97		return fmt.Errorf("%s: missing server endpoint", modName)
 98	}
 99
100	endp, err := config.ParseEndpoint(a.serverEndpoint)
101	if err != nil {
102		return fmt.Errorf("%s: invalid server endpoint: %v", modName, err)
103	}
104
105	// Dial once to check usability and also to get list of mechanisms.
106	conn, err := net.Dial(endp.Scheme, endp.Address())
107	if err != nil {
108		return fmt.Errorf("%s: unable to contact server: %v", modName, err)
109	}
110
111	cl, err := dovecotsasl.NewClient(conn)
112	if err != nil {
113		return fmt.Errorf("%s: unable to contact server: %v", modName, err)
114	}
115
116	defer cl.Close()
117	a.mechanisms = make(map[string]dovecotsasl.Mechanism, len(cl.ConnInfo().Mechs))
118	for name, mech := range cl.ConnInfo().Mechs {
119		if mech.Private {
120			continue
121		}
122		a.mechanisms[name] = mech
123	}
124
125	a.network = endp.Scheme
126	a.addr = endp.Address()
127
128	return nil
129}
130
131func (a *Auth) AuthPlain(username, password string) error {
132	if _, ok := a.mechanisms[sasl.Plain]; ok {
133		cl, err := a.getConn()
134		if err != nil {
135			return exterrors.WithTemporary(err, true)
136		}
137		defer a.returnConn(cl)
138
139		// Pretend it is SMTPS even though we really don't know.
140		// We also have no connection information to pass to the server...
141		return cl.Do("SMTP", sasl.NewPlainClient("", username, password),
142			dovecotsasl.Secured, dovecotsasl.NoPenalty)
143	}
144	if _, ok := a.mechanisms[sasl.Login]; ok {
145		cl, err := a.getConn()
146		if err != nil {
147			return err
148		}
149		defer a.returnConn(cl)
150
151		return cl.Do("SMTP", sasl.NewLoginClient(username, password),
152			dovecotsasl.Secured, dovecotsasl.NoPenalty)
153	}
154
155	return auth.ErrUnsupportedMech
156}
157
158func init() {
159	module.Register(modName, New)
160}