Skip to content

Commit

Permalink
Merge remote-tracking branch 'angelnu/1-n' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
foxcpp committed Feb 19, 2022
2 parents 2677e19 + ef5ca38 commit bc42464
Show file tree
Hide file tree
Showing 21 changed files with 411 additions and 211 deletions.
16 changes: 13 additions & 3 deletions docs/reference/modifiers/envelope.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Envelope sender / recipient rewriting

'replace\_sender' and 'replace\_rcpt' modules replace SMTP envelope addresses
based on the mapping defined by the table module. Currently,
only 1:1 mappings are supported (that is, it is not possible to specify
multiple replacements for a single address).
based on the mapping defined by the table module (maddy-tables(5)). It is possible
to specify 1:N mappings. This allows, for example, implementing mailing lists.

The address is normalized before lookup (Punycode in domain-part is decoded,
Unicode is normalized to NFC, the whole string is case-folded).
Expand Down Expand Up @@ -33,8 +32,10 @@ modify {
replace_rcpt file /etc/maddy/aliases
replace_rcpt static {
entry a@example.org b@example.org
entry c@example.org c1@example.org c2@example.org
}
replace_rcpt regexp "(.+)@example.net" "$1@example.org"
replace_rcpt regexp "(.+)@example.net" "$1@example.org" "$1@example.com"
}
```

Expand All @@ -47,4 +48,13 @@ cat: dog
# Replace cat@example.org with cat@example.com.
# Takes priority over the previous line.
cat@example.org: cat@example.com
# Using aliases in multiple lines
cat2: dog
cat2: mouse
cat2@example.org: cat@example.com
cat2@example.org: cat@example.net
# Comma-separated aliases in multiple lines
cat3: dog , mouse
cat3@example.org: cat@example.com , cat@example.net
```
10 changes: 7 additions & 3 deletions docs/reference/table/file.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,14 @@ aaa: bbb
aaa
# If the same key is used multiple times - table.file will return
# multiple values when queries. Note that this is not used by
# most modules. E.g. replace_rcpt does not (intentionally) support
# 1-to-N alias expansion.
# multiple values when queries.
ddd: firstvalue
ddd: secondvalue
# Alternatively, multiple values can be specified
# using a comma. There is no support for escaping
# so you would have to use a different format if you require
# comma-separated values.
ddd: firstvalue, secondvalue
```

4 changes: 4 additions & 0 deletions framework/module/dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ func (d *Dummy) Lookup(_ context.Context, _ string) (string, bool, error) {
return "", false, nil
}

func (d *Dummy) LookupMulti(_ context.Context, _ string) ([]string, error) {
return []string{""}, nil
}

func (d *Dummy) Name() string {
return "dummy"
}
Expand Down
6 changes: 3 additions & 3 deletions framework/module/modifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ type ModifierState interface {
RewriteSender(ctx context.Context, mailFrom string) (string, error)

// RewriteRcpt replaces RCPT TO value.
// If no changed are required, this method returns its argument, otherwise
// it returns a new value.
// If no changed are required, this method returns its argument as slice,
// otherwise it returns a slice with 1 or more new values.
//
// MsgPipeline will take of populating MsgMeta.OriginalRcpts. RewriteRcpt
// doesn't do it.
RewriteRcpt(ctx context.Context, rcptTo string) (string, error)
RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error)

// RewriteBody modifies passed Header argument and may optionally
// inspect the passed body buffer to make a decision on new header field values.
Expand Down
42 changes: 22 additions & 20 deletions internal/authz/lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,33 @@ import (
"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)
func AuthorizeEmailUse(ctx context.Context, username string, addrs []string, mapping module.Table) (bool, error) {
for _, addr := range addrs {
_, domain, err := address.Split(addr)
if err != nil {
return false, fmt.Errorf("authz: %w", err)
}
} else {
validEmail, ok, err := mapping.Lookup(ctx, username)
if err != nil {
return false, fmt.Errorf("authz: %w", err)
}
if ok {
validEmails = []string{validEmail}

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(ctx, 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
for _, ent := range validEmails {
if ent == domain || ent == "*" || ent == addr {
return true, nil
}
}
}

Expand Down
15 changes: 12 additions & 3 deletions internal/check/authorize_sender/authorize_sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,18 @@ func (s *state) authzSender(ctx context.Context, authName, email string) module.
}})
}

var preparedEmail []string
var ok bool
s.log.DebugMsg("normalized names", "from", fromEmailNorm, "auth", authNameNorm)

preparedEmail, ok, err := s.c.emailPrepare.Lookup(ctx, fromEmailNorm)
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{
Expand All @@ -176,7 +185,7 @@ func (s *state) authzSender(ctx context.Context, authName, email string) module.
}})
}
if !ok {
preparedEmail = fromEmailNorm
preparedEmail = []string{fromEmailNorm}
}

ok, err = authz.AuthorizeEmailUse(ctx, authNameNorm, preparedEmail, s.c.userToEmail)
Expand Down
4 changes: 2 additions & 2 deletions internal/modify/dkim/dkim.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,8 @@ func (s *state) RewriteSender(ctx context.Context, mailFrom string) (string, err
return mailFrom, nil
}

func (s state) RewriteRcpt(ctx context.Context, rcptTo string) (string, error) {
return rcptTo, nil
func (s state) RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error) {
return []string{rcptTo}, nil
}

func (s *state) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error {
Expand Down
17 changes: 12 additions & 5 deletions internal/modify/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,22 @@ func (gs groupState) RewriteSender(ctx context.Context, mailFrom string) (string
return mailFrom, nil
}

func (gs groupState) RewriteRcpt(ctx context.Context, rcptTo string) (string, error) {
func (gs groupState) RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error) {
var err error
var result = []string{rcptTo}
for _, state := range gs.states {
rcptTo, err = state.RewriteRcpt(ctx, rcptTo)
if err != nil {
return "", err
var intermediateResult = []string{}
for _, partResult := range result {
var partResult_multi []string
partResult_multi, err = state.RewriteRcpt(ctx, partResult)
if err != nil {
return []string{""}, err
}
intermediateResult = append(intermediateResult, partResult_multi...)
}
result = intermediateResult
}
return rcptTo, nil
return result, nil
}

func (gs groupState) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error {
Expand Down
55 changes: 33 additions & 22 deletions internal/modify/replace_addr.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type replaceAddr struct {

replaceSender bool
replaceRcpt bool
table module.Table
table module.MultiTable
}

func NewReplaceAddr(modName, instName string, _, inlineArgs []string) (module.Module, error) {
Expand Down Expand Up @@ -76,16 +76,20 @@ func (r replaceAddr) ModStateForMsg(ctx context.Context, msgMeta *module.MsgMeta

func (r replaceAddr) RewriteSender(ctx context.Context, mailFrom string) (string, error) {
if r.replaceSender {
return r.rewrite(ctx, mailFrom)
results, err := r.rewrite(ctx, mailFrom)
if err != nil {
return mailFrom, err
}
mailFrom = results[0]
}
return mailFrom, nil
}

func (r replaceAddr) RewriteRcpt(ctx context.Context, rcptTo string) (string, error) {
func (r replaceAddr) RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error) {
if r.replaceRcpt {
return r.rewrite(ctx, rcptTo)
}
return rcptTo, nil
return []string{rcptTo}, nil
}

func (r replaceAddr) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error {
Expand All @@ -96,47 +100,54 @@ func (r replaceAddr) Close() error {
return nil
}

func (r replaceAddr) rewrite(ctx context.Context, val string) (string, error) {
func (r replaceAddr) rewrite(ctx context.Context, val string) ([]string, error) {
normAddr, err := address.ForLookup(val)
if err != nil {
return val, fmt.Errorf("malformed address: %v", err)
return []string{val}, fmt.Errorf("malformed address: %v", err)
}

replacement, ok, err := r.table.Lookup(ctx, normAddr)
replacements, err := r.table.LookupMulti(ctx, normAddr)
if err != nil {
return val, err
return []string{val}, err
}
if ok {
if !address.Valid(replacement) {
return "", fmt.Errorf("refusing to replace recipient with the invalid address %s", replacement)
if len(replacements) > 0 {
for _, replacement := range replacements {
if !address.Valid(replacement) {
return []string{""}, fmt.Errorf("refusing to replace recipient with the invalid address %s", replacement)
}
}
return replacement, nil
return replacements, nil
}

mbox, domain, err := address.Split(normAddr)
if err != nil {
// If we have malformed address here, something is really wrong, but let's
// ignore it silently then anyway.
return val, nil
return []string{val}, nil
}

// mbox is already normalized, since it is a part of address.ForLookup
// result.
replacement, ok, err = r.table.Lookup(ctx, mbox)
replacements, err = r.table.LookupMulti(ctx, mbox)
if err != nil {
return val, err
return []string{val}, err
}
if ok {
if strings.Contains(replacement, "@") && !strings.HasPrefix(replacement, `"`) && !strings.HasSuffix(replacement, `"`) {
if !address.Valid(replacement) {
return "", fmt.Errorf("refusing to replace recipient with invalid address %s", replacement)
if len(replacements) > 0 {
var results = make([]string, len(replacements))
for i, replacement := range replacements {
if strings.Contains(replacement, "@") && !strings.HasPrefix(replacement, `"`) && !strings.HasSuffix(replacement, `"`) {
if !address.Valid(replacement) {
return []string{""}, fmt.Errorf("refusing to replace recipient with invalid address %s", replacement)
}
results[i] = replacement
} else {
results[i] = replacement + "@" + domain
}
return replacement, nil
}
return replacement + "@" + domain, nil
return results, nil
}

return val, nil
return []string{val}, nil
}

func init() {
Expand Down
Loading

0 comments on commit bc42464

Please sign in to comment.