maddy/internal/check/authorize_sender/authorize_sender.go
2025-09-05 16:54:41 +02:00

306 lines
8.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 authorize_sender
import (
"context"
"net/mail"
"github.com/emersion/go-message/textproto"
"github.com/foxcpp/maddy/framework/buffer"
"github.com/foxcpp/maddy/framework/config"
modconfig "github.com/foxcpp/maddy/framework/config/module"
"github.com/foxcpp/maddy/framework/exterrors"
"github.com/foxcpp/maddy/framework/log"
"github.com/foxcpp/maddy/framework/module"
"github.com/foxcpp/maddy/internal/authz"
"github.com/foxcpp/maddy/internal/table"
"github.com/foxcpp/maddy/internal/target"
)
const modName = "check.authorize_sender"
type Check struct {
instName string
log log.Logger
checkHeader bool
emailPrepare module.Table
userToEmail module.Table
unauthAction modconfig.FailAction
noMatchAction modconfig.FailAction
errAction modconfig.FailAction
fromNorm authz.NormalizeFunc
authNorm authz.NormalizeFunc
}
func New(_, instName string, _, inlineArgs []string) (module.Module, error) {
return &Check{
instName: instName,
}, nil
}
func (c *Check) Name() string {
return modName
}
func (c *Check) InstanceName() string {
return c.instName
}
func (c *Check) Init(cfg *config.Map) error {
cfg.Bool("debug", true, false, &c.log.Debug)
cfg.Bool("check_header", false, true, &c.checkHeader)
cfg.Custom("prepare_email", false, false, func() (interface{}, error) {
return &table.Identity{}, nil
}, modconfig.TableDirective, &c.emailPrepare)
cfg.Custom("user_to_email", false, false, func() (interface{}, error) {
return &table.Identity{}, nil
}, modconfig.TableDirective, &c.userToEmail)
cfg.Custom("unauth_action", false, false, func() (interface{}, error) {
return modconfig.FailAction{Reject: true}, nil
}, modconfig.FailActionDirective, &c.unauthAction)
cfg.Custom("no_match_action", false, false, func() (interface{}, error) {
return modconfig.FailAction{Reject: true}, nil
}, modconfig.FailActionDirective, &c.noMatchAction)
cfg.Custom("err_action", false, false, func() (interface{}, error) {
return modconfig.FailAction{Reject: true}, nil
}, modconfig.FailActionDirective, &c.errAction)
config.EnumMapped(cfg, "auth_normalize", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,
&c.authNorm)
config.EnumMapped(cfg, "from_normalize", true, false, authz.NormalizeFuncs, authz.NormalizeAuto,
&c.fromNorm)
if _, err := cfg.Process(); err != nil {
return err
}
return nil
}
type state struct {
c *Check
msgMeta *module.MsgMetadata
log log.Logger
}
func (c *Check) CheckStateForMsg(_ context.Context, msgMeta *module.MsgMetadata) (module.CheckState, error) {
return &state{
c: c,
msgMeta: msgMeta,
log: target.DeliveryLogger(c.log, msgMeta),
}, nil
}
func (s *state) authzSender(ctx context.Context, authName, email string) module.CheckResult {
if authName == "" {
return s.c.unauthAction.Apply(module.CheckResult{
Reason: &exterrors.SMTPError{
Code: 530,
EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
Message: "Authentication required",
CheckName: modName,
}})
}
fromEmailNorm, err := s.c.fromNorm(email)
if err != nil {
return s.c.errAction.Apply(module.CheckResult{
Reason: &exterrors.SMTPError{
Code: 553,
EnhancedCode: exterrors.EnhancedCode{5, 1, 7},
Message: "Unable to normalize sender address",
CheckName: modName,
Err: err,
}})
}
authNameNorm, err := s.c.authNorm(authName)
if err != nil {
return s.c.errAction.Apply(module.CheckResult{
Reason: &exterrors.SMTPError{
Code: 535,
EnhancedCode: exterrors.EnhancedCode{5, 7, 8},
Message: "Unable to normalize authorization username",
CheckName: modName,
}})
}
var preparedEmail []string
var ok bool
s.log.DebugMsg("normalized names", "from", fromEmailNorm, "auth", authNameNorm)
if emailPrepareMulti, isMulti := s.c.emailPrepare.(module.MultiTable); isMulti {
preparedEmail, err = emailPrepareMulti.LookupMulti(ctx, fromEmailNorm)
ok = len(preparedEmail) > 0
} else {
var preparedEmail_single string
preparedEmail_single, ok, err = s.c.emailPrepare.Lookup(ctx, fromEmailNorm)
preparedEmail = []string{preparedEmail_single}
}
s.log.DebugMsg("authorized emails", "preparedEmail", preparedEmail, "ok", ok)
if err != nil {
return s.c.errAction.Apply(module.CheckResult{
Reason: &exterrors.SMTPError{
Code: 454,
EnhancedCode: exterrors.EnhancedCode{4, 7, 0},
Message: "Internal error during policy check",
CheckName: modName,
Err: err,
}})
}
if !ok {
preparedEmail = []string{fromEmailNorm}
}
ok, err = authz.AuthorizeEmailUse(ctx, authNameNorm, preparedEmail, s.c.userToEmail)
if err != nil {
return s.c.errAction.Apply(module.CheckResult{
Reason: &exterrors.SMTPError{
Code: 454,
EnhancedCode: exterrors.EnhancedCode{4, 7, 0},
Message: "Internal error during policy check",
CheckName: modName,
Err: err,
}})
}
if !ok {
return s.c.noMatchAction.Apply(module.CheckResult{
Reason: &exterrors.SMTPError{
Code: 553,
EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
Message: "Unauthorized use of sender address",
CheckName: modName,
}})
}
return module.CheckResult{}
}
func (s *state) CheckConnection(_ context.Context) module.CheckResult {
return module.CheckResult{}
}
func (s *state) CheckSender(ctx context.Context, fromEmail string) module.CheckResult {
if s.msgMeta.Conn == nil {
s.log.Msg("skipping locally generated message")
return module.CheckResult{}
}
authName := s.msgMeta.Conn.AuthUser
return s.authzSender(ctx, authName, fromEmail)
}
func (s *state) CheckRcpt(_ context.Context, _ string) module.CheckResult {
return module.CheckResult{}
}
func (s *state) CheckBody(ctx context.Context, hdr textproto.Header, _ buffer.Buffer) module.CheckResult {
if !s.c.checkHeader {
return module.CheckResult{}
}
if s.msgMeta.Conn == nil {
s.log.Msg("skipping locally generated message")
return module.CheckResult{}
}
authName := s.msgMeta.Conn.AuthUser
fromHdr := hdr.Get("From")
if fromHdr == "" {
return s.c.errAction.Apply(module.CheckResult{
Reason: &exterrors.SMTPError{
Code: 550,
EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
Message: "Missing From header",
CheckName: modName,
}})
}
list, err := mail.ParseAddressList(fromHdr)
if err != nil || len(list) == 0 {
return s.c.errAction.Apply(module.CheckResult{
Reason: &exterrors.SMTPError{
Code: 550,
EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
Message: "Malformed From header",
CheckName: modName,
Err: err,
}})
}
fromEmail := list[0].Address
if len(list) > 1 {
return s.c.errAction.Apply(module.CheckResult{
Reason: &exterrors.SMTPError{
Code: 550,
EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
Message: "Multiple From addresses are not allowed",
CheckName: modName,
Err: err,
}})
}
var senderAddr string
if senderHdr := hdr.Get("Sender"); senderHdr != "" {
sender, err := mail.ParseAddress(senderHdr)
if err != nil {
return s.c.errAction.Apply(module.CheckResult{
Reason: &exterrors.SMTPError{
Code: 550,
EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
Message: "Malformed Sender header",
CheckName: modName,
Err: err,
}})
}
senderAddr = sender.Address
}
res := s.authzSender(ctx, authName, fromEmail)
if res.Reason == nil {
return res
}
if senderAddr != "" && senderAddr != fromEmail {
res = s.authzSender(ctx, authName, senderAddr)
if res.Reason == nil {
return res
}
}
// Neither matched.
return s.c.noMatchAction.Apply(module.CheckResult{
Reason: &exterrors.SMTPError{
Code: 553,
EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
Message: "Unauthorized use of sender address",
CheckName: modName,
}})
}
func (s *state) Close() error {
return nil
}
func init() {
module.Register(modName, New)
}