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 authorize_sender
 20
 21import (
 22	"context"
 23	"net/mail"
 24
 25	"github.com/emersion/go-message/textproto"
 26	"github.com/foxcpp/maddy/framework/buffer"
 27	"github.com/foxcpp/maddy/framework/config"
 28	modconfig "github.com/foxcpp/maddy/framework/config/module"
 29	"github.com/foxcpp/maddy/framework/exterrors"
 30	"github.com/foxcpp/maddy/framework/log"
 31	"github.com/foxcpp/maddy/framework/module"
 32	"github.com/foxcpp/maddy/internal/authz"
 33	"github.com/foxcpp/maddy/internal/table"
 34	"github.com/foxcpp/maddy/internal/target"
 35)
 36
 37const modName = "check.authorize_sender"
 38
 39type Check struct {
 40	instName string
 41	log      log.Logger
 42
 43	checkHeader  bool
 44	emailPrepare module.Table
 45	userToEmail  module.Table
 46
 47	unauthAction  modconfig.FailAction
 48	noMatchAction modconfig.FailAction
 49	errAction     modconfig.FailAction
 50
 51	fromNorm authz.NormalizeFunc
 52	authNorm authz.NormalizeFunc
 53}
 54
 55func New(_, instName string, _, inlineArgs []string) (module.Module, error) {
 56	return &Check{
 57		instName: instName,
 58	}, nil
 59}
 60
 61func (c *Check) Name() string {
 62	return modName
 63}
 64
 65func (c *Check) InstanceName() string {
 66	return c.instName
 67}
 68
 69func (c *Check) Init(cfg *config.Map) error {
 70	cfg.Bool("debug", true, false, &c.log.Debug)
 71
 72	cfg.Bool("check_header", false, true, &c.checkHeader)
 73
 74	cfg.Custom("prepare_email", false, false, func() (interface{}, error) {
 75		return &table.Identity{}, nil
 76	}, modconfig.TableDirective, &c.emailPrepare)
 77	cfg.Custom("user_to_email", false, false, func() (interface{}, error) {
 78		return &table.Identity{}, nil
 79	}, modconfig.TableDirective, &c.userToEmail)
 80
 81	cfg.Custom("unauth_action", false, false, func() (interface{}, error) {
 82		return modconfig.FailAction{Reject: true}, nil
 83	}, modconfig.FailActionDirective, &c.unauthAction)
 84	cfg.Custom("no_match_action", false, false, func() (interface{}, error) {
 85		return modconfig.FailAction{Reject: true}, nil
 86	}, modconfig.FailActionDirective, &c.noMatchAction)
 87	cfg.Custom("err_action", false, false, func() (interface{}, error) {
 88		return modconfig.FailAction{Reject: true}, nil
 89	}, modconfig.FailActionDirective, &c.errAction)
 90
 91	config.EnumMapped(cfg, "auth_normalize", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,
 92		&c.authNorm)
 93	config.EnumMapped(cfg, "from_normalize", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,
 94		&c.fromNorm)
 95
 96	if _, err := cfg.Process(); err != nil {
 97		return err
 98	}
 99
100	return nil
101}
102
103type state struct {
104	c       *Check
105	msgMeta *module.MsgMetadata
106	log     log.Logger
107}
108
109func (c *Check) CheckStateForMsg(_ context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {
110	return &state{
111		c:       c,
112		msgMeta: msgMeta,
113		log:     target.DeliveryLogger(c.log, msgMeta),
114	}, nil
115}
116
117func (s *state) authzSender(ctx context.Context, authName, email string) module.CheckResult {
118	if authName == "" {
119		return s.c.unauthAction.Apply(module.CheckResult{
120			Reason: &exterrors.SMTPError{
121				Code:         530,
122				EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
123				Message:      "Authentication required",
124				CheckName:    modName,
125			}})
126	}
127
128	fromEmailNorm, err := s.c.fromNorm(email)
129	if err != nil {
130		return s.c.errAction.Apply(module.CheckResult{
131			Reason: &exterrors.SMTPError{
132				Code:         553,
133				EnhancedCode: exterrors.EnhancedCode{5, 1, 7},
134				Message:      "Unable to normalize sender address",
135				CheckName:    modName,
136				Err:          err,
137			}})
138	}
139	authNameNorm, err := s.c.authNorm(authName)
140	if err != nil {
141		return s.c.errAction.Apply(module.CheckResult{
142			Reason: &exterrors.SMTPError{
143				Code:         535,
144				EnhancedCode: exterrors.EnhancedCode{5, 7, 8},
145				Message:      "Unable to normalize authorization username",
146				CheckName:    modName,
147			}})
148	}
149
150	var preparedEmail []string
151	var ok bool
152	s.log.DebugMsg("normalized names", "from", fromEmailNorm, "auth", authNameNorm)
153	if emailPrepareMulti, isMulti := s.c.emailPrepare.(module.MultiTable); isMulti {
154		preparedEmail, err = emailPrepareMulti.LookupMulti(ctx, fromEmailNorm)
155		ok = len(preparedEmail) > 0
156	} else {
157		var preparedEmail_single string
158		preparedEmail_single, ok, err = s.c.emailPrepare.Lookup(ctx, fromEmailNorm)
159		preparedEmail = []string{preparedEmail_single}
160	}
161	s.log.DebugMsg("authorized emails", "preparedEmail", preparedEmail, "ok", ok)
162	if err != nil {
163		return s.c.errAction.Apply(module.CheckResult{
164			Reason: &exterrors.SMTPError{
165				Code:         454,
166				EnhancedCode: exterrors.EnhancedCode{4, 7, 0},
167				Message:      "Internal error during policy check",
168				CheckName:    modName,
169				Err:          err,
170			}})
171	}
172	if !ok {
173		preparedEmail = []string{fromEmailNorm}
174	}
175
176	ok, err = authz.AuthorizeEmailUse(ctx, authNameNorm, preparedEmail, s.c.userToEmail)
177	if err != nil {
178		return s.c.errAction.Apply(module.CheckResult{
179			Reason: &exterrors.SMTPError{
180				Code:         454,
181				EnhancedCode: exterrors.EnhancedCode{4, 7, 0},
182				Message:      "Internal error during policy check",
183				CheckName:    modName,
184				Err:          err,
185			}})
186	}
187	if !ok {
188		return s.c.noMatchAction.Apply(module.CheckResult{
189			Reason: &exterrors.SMTPError{
190				Code:         553,
191				EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
192				Message:      "Unauthorized use of sender address",
193				CheckName:    modName,
194			}})
195	}
196
197	return module.CheckResult{}
198}
199
200func (s *state) CheckConnection(_ context.Context) module.CheckResult {
201	return module.CheckResult{}
202}
203
204func (s *state) CheckSender(ctx context.Context, fromEmail string) module.CheckResult {
205	if s.msgMeta.Conn == nil {
206		s.log.Msg("skipping locally generated message")
207		return module.CheckResult{}
208	}
209	authName := s.msgMeta.Conn.AuthUser
210
211	return s.authzSender(ctx, authName, fromEmail)
212}
213
214func (s *state) CheckRcpt(_ context.Context, _ string) module.CheckResult {
215	return module.CheckResult{}
216}
217
218func (s *state) CheckBody(ctx context.Context, hdr textproto.Header, _ buffer.Buffer) module.CheckResult {
219	if !s.c.checkHeader {
220		return module.CheckResult{}
221	}
222	if s.msgMeta.Conn == nil {
223		s.log.Msg("skipping locally generated message")
224		return module.CheckResult{}
225	}
226	authName := s.msgMeta.Conn.AuthUser
227
228	fromHdr := hdr.Get("From")
229	if fromHdr == "" {
230		return s.c.errAction.Apply(module.CheckResult{
231			Reason: &exterrors.SMTPError{
232				Code:         550,
233				EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
234				Message:      "Missing From header",
235				CheckName:    modName,
236			}})
237	}
238	list, err := mail.ParseAddressList(fromHdr)
239	if err != nil || len(list) == 0 {
240		return s.c.errAction.Apply(module.CheckResult{
241			Reason: &exterrors.SMTPError{
242				Code:         550,
243				EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
244				Message:      "Malformed From header",
245				CheckName:    modName,
246				Err:          err,
247			}})
248	}
249	fromEmail := list[0].Address
250	if len(list) > 1 {
251		return s.c.errAction.Apply(module.CheckResult{
252			Reason: &exterrors.SMTPError{
253				Code:         550,
254				EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
255				Message:      "Multiple From addresses are not allowed",
256				CheckName:    modName,
257				Err:          err,
258			}})
259	}
260
261	var senderAddr string
262	if senderHdr := hdr.Get("Sender"); senderHdr != "" {
263		sender, err := mail.ParseAddress(senderHdr)
264		if err != nil {
265			return s.c.errAction.Apply(module.CheckResult{
266				Reason: &exterrors.SMTPError{
267					Code:         550,
268					EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
269					Message:      "Malformed Sender header",
270					CheckName:    modName,
271					Err:          err,
272				}})
273		}
274		senderAddr = sender.Address
275	}
276
277	res := s.authzSender(ctx, authName, fromEmail)
278	if res.Reason == nil {
279		return res
280	}
281
282	if senderAddr != "" && senderAddr != fromEmail {
283		res = s.authzSender(ctx, authName, senderAddr)
284		if res.Reason == nil {
285			return res
286		}
287	}
288
289	// Neither matched.
290	return s.c.noMatchAction.Apply(module.CheckResult{
291		Reason: &exterrors.SMTPError{
292			Code:         553,
293			EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
294			Message:      "Unauthorized use of sender address",
295			CheckName:    modName,
296		}})
297}
298
299func (s *state) Close() error {
300	return nil
301}
302
303func init() {
304	module.Register(modName, New)
305}