mirror of
https://github.com/foxcpp/maddy.git
synced 2025-04-04 21:47:40 +03:00
check/authorize_sender: Implement MAIL FROM, From header authorization for local senders
Closes #268.
This commit is contained in:
parent
38cfb981ec
commit
7c2afde847
10 changed files with 600 additions and 31 deletions
|
@ -872,3 +872,91 @@ X-Spam-Flag and X-Spam-Score are added to the header irregardless of value.
|
|||
|
||||
Flags to pass to the rspamd server.
|
||||
See https://rspamd.com/doc/architecture/protocol.html for details.
|
||||
|
||||
## MAIL FROM and From authorization (check.authorize_sender)
|
||||
|
||||
This check verifies that envelope and header sender addresses belong
|
||||
to the authenticated user. Address ownership is established via table
|
||||
that maps each user account to a email address it is allowed to use.
|
||||
There are some special cases, see user_to_email description below.
|
||||
|
||||
```
|
||||
check.authorize_sender {
|
||||
prepare_email identity
|
||||
user_to_email identity
|
||||
check_header yes
|
||||
|
||||
unauth_action reject
|
||||
no_match_action reject
|
||||
malformed_action reject
|
||||
err_action reject
|
||||
|
||||
auth_normalize precis_casefold_email
|
||||
from_normalize precis_casefold_email
|
||||
}
|
||||
```
|
||||
```
|
||||
check {
|
||||
authorize_sender { ... }
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration directives
|
||||
|
||||
*Syntax:* user_to_email _table_ ++
|
||||
*Default:* identity
|
||||
|
||||
Table to use for lookups. Result of the lookup should contain either the
|
||||
domain name, the full email address or "*" string. If it is just domain - user
|
||||
will be allowed to use any mailbox within a domain as a sender address.
|
||||
If result contains "*" - user will be allowed to use any address.
|
||||
|
||||
*Syntax:* check_header _boolean_ ++
|
||||
*Default:* yes
|
||||
|
||||
Whether to verify header sender in addition to envelope.
|
||||
|
||||
Either Sender or From field value should match the
|
||||
authorization identity.
|
||||
|
||||
*Syntax:* unauth_action _action_ ++
|
||||
*Default:* reject
|
||||
|
||||
What to do if the user is not authenticated at all.
|
||||
|
||||
*Syntax:* no_match_action _action_ ++
|
||||
*Default:* reject
|
||||
|
||||
What to do if user is not allowed to use the sender address specified.
|
||||
|
||||
*Syntax:* malformed_action _action_ ++
|
||||
*Default:* reject
|
||||
|
||||
What to do if From or Sender header fields contain malformed values.
|
||||
|
||||
*Syntax:* err_action _action_ ++
|
||||
*Default:* reject
|
||||
|
||||
What to do if error happens during prepare_email or user_to_email lookup.
|
||||
|
||||
*Syntax:* auth_normalize _action_ ++
|
||||
*Default:* precis_casefold_email
|
||||
|
||||
Normalization function to apply to authorization username before
|
||||
further processing.
|
||||
|
||||
Available options:
|
||||
- precis_casefold_email PRECIS UsernameCaseMapped profile + U-labels form for domain
|
||||
- precis_casefold PRECIS UsernameCaseMapped profile for the entire string
|
||||
- precis_email PRECIS UsernameCasePreserved profile + U-labels form for domain
|
||||
- precis PRECIS UsernameCasePreserved profile for the entire string
|
||||
- casefold Convert to lower case
|
||||
- noop Nothing
|
||||
|
||||
*Syntax:* from_normalize _action_ ++
|
||||
*Default:* precis_casefold_email
|
||||
|
||||
Normalization function to apply to email addresses before
|
||||
further processing.
|
||||
|
||||
Available options are same as for auth_normalize.
|
|
@ -19,11 +19,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
package address
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/foxcpp/maddy/framework/dns"
|
||||
"golang.org/x/net/idna"
|
||||
"golang.org/x/text/secure/precis"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
|
@ -112,3 +114,41 @@ func IsASCII(s string) bool {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// PRECISFold applies UsernameCaseMapped to the local part and dns.ForLookup
|
||||
// to domain part of the address.
|
||||
func PRECISFold(addr string) (string, error) {
|
||||
return precisEmail(addr, precis.UsernameCaseMapped)
|
||||
}
|
||||
|
||||
// PRECIS applies UsernameCasePreserved to the local part and dns.ForLookup
|
||||
// to domain part of the address.
|
||||
func PRECIS(addr string) (string, error) {
|
||||
return precisEmail(addr, precis.UsernameCasePreserved)
|
||||
}
|
||||
|
||||
func precisEmail(addr string, profile *precis.Profile) (string, error) {
|
||||
mbox, domain, err := Split(addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("address: precis: %w", err)
|
||||
}
|
||||
|
||||
// PRECISFold is not included in the regular address.ForLookup since it reduces
|
||||
// the range of valid addresses to a subset of actually valid values.
|
||||
// PRECISFold is a matter of our own local policy, not a general rule for all
|
||||
// email addresses.
|
||||
|
||||
// Side note: For used profiles, there is no practical difference between
|
||||
// CompareKey and String.
|
||||
mbox, err = profile.CompareKey(mbox)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("address: precis: %w", err)
|
||||
}
|
||||
|
||||
domain, err = dns.ForLookup(domain)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("address: precis: %w", err)
|
||||
}
|
||||
|
||||
return mbox + "@" + domain, nil
|
||||
}
|
||||
|
|
|
@ -18,7 +18,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
package module
|
||||
|
||||
// Tabele is the interface implemented by module that implementation string-to-string
|
||||
import "context"
|
||||
|
||||
// Table is the interface implemented by module that implementation string-to-string
|
||||
// translation.
|
||||
//
|
||||
// Modules implementing this interface should be registered with prefix
|
||||
|
@ -27,6 +29,12 @@ type Table interface {
|
|||
Lookup(s string) (string, bool, error)
|
||||
}
|
||||
|
||||
// MultiTable is the interface that module can implement in addition to Table
|
||||
// if it can provide multiple values as a lookup result.
|
||||
type MultiTable interface {
|
||||
LookupMulti(ctx context.Context, s string) ([]string, error)
|
||||
}
|
||||
|
||||
type MutableTable interface {
|
||||
Table
|
||||
Keys() ([]string, error)
|
||||
|
|
40
internal/authz/lookup.go
Normal file
40
internal/authz/lookup.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/foxcpp/maddy/framework/address"
|
||||
"github.com/foxcpp/maddy/framework/module"
|
||||
)
|
||||
|
||||
func AuthorizeEmailUse(ctx context.Context, username, addr string, mapping module.Table) (bool, error) {
|
||||
_, domain, err := address.Split(addr)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("authz: %w", err)
|
||||
}
|
||||
|
||||
var validEmails []string
|
||||
if multi, ok := mapping.(module.MultiTable); ok {
|
||||
validEmails, err = multi.LookupMulti(ctx, username)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("authz: %w", err)
|
||||
}
|
||||
} else {
|
||||
validEmail, ok, err := mapping.Lookup(username)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("authz: %w", err)
|
||||
}
|
||||
if ok {
|
||||
validEmails = []string{validEmail}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ent := range validEmails {
|
||||
if ent == domain || ent == "*" || ent == addr {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
23
internal/authz/normalization.go
Normal file
23
internal/authz/normalization.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package authz
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/foxcpp/maddy/framework/address"
|
||||
"golang.org/x/text/secure/precis"
|
||||
)
|
||||
|
||||
// NormalizeFuncs defines configurable normalization functions to be used
|
||||
// in authentication and authorization routines.
|
||||
var NormalizeFuncs = map[string]func(string) (string, error){
|
||||
"precis_casefold_email": address.PRECISFold,
|
||||
"precis_casefold": precis.UsernameCaseMapped.CompareKey,
|
||||
"precis_email": address.PRECIS,
|
||||
"precis": precis.UsernameCasePreserved.CompareKey,
|
||||
"casefold": func(s string) (string, error) {
|
||||
return strings.ToLower(s), nil
|
||||
},
|
||||
"noop": func(s string) (string, error) {
|
||||
return s, nil
|
||||
},
|
||||
}
|
311
internal/check/authorize_sender/authorize_sender.go
Normal file
311
internal/check/authorize_sender/authorize_sender.go
Normal file
|
@ -0,0 +1,311 @@
|
|||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"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 func(string) (string, error)
|
||||
authNorm func(string) (string, error)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
var (
|
||||
authNormalize string
|
||||
fromNormalize string
|
||||
ok bool
|
||||
)
|
||||
cfg.String("auth_normalize", false, false,
|
||||
"precis_casefold_email", &authNormalize)
|
||||
cfg.String("from_normalize", false, false,
|
||||
"precis_casefold_email", &fromNormalize)
|
||||
|
||||
if _, err := cfg.Process(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.authNorm, ok = authz.NormalizeFuncs[authNormalize]
|
||||
if !ok {
|
||||
return fmt.Errorf("%v: unknown normalization function: %v", modName, authNormalize)
|
||||
}
|
||||
c.fromNorm, ok = authz.NormalizeFuncs[fromNormalize]
|
||||
if !ok {
|
||||
return fmt.Errorf("%v: unknown normalization function: %v", modName, fromNormalize)
|
||||
}
|
||||
|
||||
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,
|
||||
}})
|
||||
}
|
||||
|
||||
s.log.DebugMsg("normalized names", "from", fromEmailNorm, "auth", authNameNorm)
|
||||
|
||||
preparedEmail, ok, err := s.c.emailPrepare.Lookup(fromEmailNorm)
|
||||
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 = 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)
|
||||
}
|
|
@ -19,43 +19,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
package imapsql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/foxcpp/maddy/framework/address"
|
||||
"github.com/foxcpp/maddy/framework/dns"
|
||||
"golang.org/x/text/secure/precis"
|
||||
)
|
||||
|
||||
var normalizeFuncs = map[string]func(string) (string, error){
|
||||
"precis_email": func(s string) (string, error) {
|
||||
mbox, domain, err := address.Split(s)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("imapsql: username prepare: %w", err)
|
||||
}
|
||||
|
||||
// PRECIS is not included in the regular address.ForLookup since it reduces
|
||||
// the range of valid addresses to a subset of actually valid values.
|
||||
// PRECIS is a matter of our own local policy, not a general rule for all
|
||||
// email addresses.
|
||||
|
||||
// Side note: For used profiles, there is no practical difference between
|
||||
// CompareKey and String.
|
||||
mbox, err = precis.UsernameCaseMapped.CompareKey(mbox)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("imapsql: username prepare: %w", err)
|
||||
}
|
||||
|
||||
domain, err = dns.ForLookup(domain)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("imapsql: username prepare: %w", err)
|
||||
}
|
||||
|
||||
return mbox + "@" + domain, nil
|
||||
},
|
||||
"precis": func(s string) (string, error) {
|
||||
return precis.UsernameCaseMapped.CompareKey(s)
|
||||
},
|
||||
"precis_email": address.PRECISFold,
|
||||
"precis": precis.UsernameCaseMapped.CompareKey,
|
||||
"casefold": func(s string) (string, error) {
|
||||
return strings.ToLower(s), nil
|
||||
},
|
||||
|
|
1
maddy.go
1
maddy.go
|
@ -44,6 +44,7 @@ import (
|
|||
_ "github.com/foxcpp/maddy/internal/auth/pass_table"
|
||||
_ "github.com/foxcpp/maddy/internal/auth/plain_separate"
|
||||
_ "github.com/foxcpp/maddy/internal/auth/shadow"
|
||||
_ "github.com/foxcpp/maddy/internal/check/authorize_sender"
|
||||
_ "github.com/foxcpp/maddy/internal/check/command"
|
||||
_ "github.com/foxcpp/maddy/internal/check/dkim"
|
||||
_ "github.com/foxcpp/maddy/internal/check/dns"
|
||||
|
|
|
@ -21,6 +21,7 @@ package tests
|
|||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
@ -201,6 +202,20 @@ func (c *Conn) TLS() {
|
|||
c.Scanner = bufio.NewScanner(c.Conn)
|
||||
}
|
||||
|
||||
func (c *Conn) SMTPPlainAuth(username, password string, expectOk bool) {
|
||||
c.T.Helper()
|
||||
|
||||
resp := append([]byte{0x00}, username...)
|
||||
resp = append(resp, 0x00)
|
||||
resp = append(resp, password...)
|
||||
c.Writeln("AUTH PLAIN " + base64.StdEncoding.EncodeToString(resp))
|
||||
if expectOk {
|
||||
c.ExpectPattern("235 *")
|
||||
} else {
|
||||
c.ExpectPattern("*")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) SMTPNegotation(ourName string, requireExts, blacklistExts []string) {
|
||||
c.T.Helper()
|
||||
|
||||
|
|
|
@ -343,6 +343,77 @@ func TestDNSBLConfig2(tt *testing.T) {
|
|||
conn.ExpectPattern("221 *")
|
||||
}
|
||||
|
||||
func TestCheckAuthorizeSender(tt *testing.T) {
|
||||
tt.Parallel()
|
||||
t := tests.NewT(tt)
|
||||
t.DNS(nil)
|
||||
t.Port("smtp")
|
||||
t.Config(`
|
||||
smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
|
||||
hostname mx.maddy.test
|
||||
tls off
|
||||
|
||||
auth dummy
|
||||
defer_sender_reject off
|
||||
|
||||
source example1.org {
|
||||
check {
|
||||
authorize_sender {
|
||||
auth_normalize precis_casefold
|
||||
user_to_email static {
|
||||
entry "test-user1" "test@example1.org"
|
||||
entry "test-user2" "é@example1.org"
|
||||
}
|
||||
}
|
||||
}
|
||||
deliver_to dummy
|
||||
}
|
||||
source example2.org {
|
||||
check {
|
||||
authorize_sender {
|
||||
auth_normalize precis_casefold
|
||||
prepare_email static {
|
||||
entry "alias-to-test@example2.org" "test@example2.org"
|
||||
}
|
||||
user_to_email static {
|
||||
entry "test-user1" "test@example2.org"
|
||||
entry "test-user2" "test2@example2.org"
|
||||
}
|
||||
}
|
||||
}
|
||||
deliver_to dummy
|
||||
}
|
||||
|
||||
default_source {
|
||||
reject
|
||||
}
|
||||
}`)
|
||||
t.Run(1)
|
||||
defer t.Close()
|
||||
|
||||
c := t.Conn("smtp")
|
||||
c.SMTPNegotation("client.maddy.test", nil, nil)
|
||||
c.SMTPPlainAuth("test-user2", "1", true)
|
||||
c.Writeln("MAIL FROM:<test@example1.org>")
|
||||
c.ExpectPattern("5*") // rejected - user is not test-user1
|
||||
c.Writeln("MAIL FROM:<test3@example1.org>")
|
||||
c.ExpectPattern("5*") // rejected - unknown email
|
||||
c.Writeln("MAIL FROM:<E\u0301@EXAMPLE1.org> SMTPUTF8")
|
||||
c.ExpectPattern("2*") // OK - é@example1.org belongs to test-user2
|
||||
c.Close()
|
||||
|
||||
c = t.Conn("smtp")
|
||||
c.SMTPNegotation("client.maddy.test", nil, nil)
|
||||
c.SMTPPlainAuth("test-user1", "1", true)
|
||||
c.Writeln("MAIL FROM:<test2@example2.org>")
|
||||
c.ExpectPattern("5*") // rejected - user is not test-user2
|
||||
c.Writeln("MAIL FROM:<test@example2.org>")
|
||||
c.ExpectPattern("2*") // OK - test@example2.org belongs to test-user
|
||||
c.Writeln("MAIL FROM:<alias-to-test@example2.org>")
|
||||
c.ExpectPattern("2*") // OK - test@example2.org belongs to test-user
|
||||
c.Close()
|
||||
}
|
||||
|
||||
func TestCheckCommand(tt *testing.T) {
|
||||
tt.Parallel()
|
||||
t := tests.NewT(tt)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue