Skip to content

Commit 3e26953

Browse files
committed
add config option to reject incoming deliveries with an error during the smtp transaction
useful when a catchall is configured, and messages to some address need to be rejected. would have been nicer if this could be part of a ruleset. but evaluating a ruleset requires us to have the message (so we can match on headers, etc). but we can't reject messages to individual recipients during the DATA command in smtp. that would reject the entire delivery attempt. for issue #156 by ally9335
1 parent 8b26e3c commit 3e26953

File tree

13 files changed

+114
-10
lines changed

13 files changed

+114
-10
lines changed

config/config.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -455,13 +455,18 @@ type JunkFilter struct {
455455
}
456456

457457
type Destination struct {
458-
Mailbox string `sconf:"optional" sconf-doc:"Mailbox to deliver to if none of Rulesets match. Default: Inbox."`
459-
Rulesets []Ruleset `sconf:"optional" sconf-doc:"Delivery rules based on message and SMTP transaction. You may want to match each mailing list by SMTP MailFrom address, VerifiedDomain and/or List-ID header (typically <listname.example.org> if the list address is [email protected]), delivering them to their own mailbox."`
460-
FullName string `sconf:"optional" sconf-doc:"Full name to use in message From header when composing messages coming from this address with webmail."`
458+
Mailbox string `sconf:"optional" sconf-doc:"Mailbox to deliver to if none of Rulesets match. Default: Inbox."`
459+
Rulesets []Ruleset `sconf:"optional" sconf-doc:"Delivery rules based on message and SMTP transaction. You may want to match each mailing list by SMTP MailFrom address, VerifiedDomain and/or List-ID header (typically <listname.example.org> if the list address is [email protected]), delivering them to their own mailbox."`
460+
SMTPError string `sconf:"optional" sconf-doc:"If non-empty, incoming delivery attempts to this destination will be rejected during SMTP RCPT TO with this error response line. Useful when a catchall address is configured for the domain and messages to some addresses should be rejected. The response line must start with an error code. Currently the following error resonse codes are allowed: 421 (temporary local error), 550 (user not found). If the line consists of only an error code, an appropriate error message is added. Rejecting messages with a 4xx code invites later retries by the remote, while 5xx codes should prevent further delivery attempts."`
461+
FullName string `sconf:"optional" sconf-doc:"Full name to use in message From header when composing messages coming from this address with webmail."`
461462

462463
DMARCReports bool `sconf:"-" json:"-"`
463464
HostTLSReports bool `sconf:"-" json:"-"`
464465
DomainTLSReports bool `sconf:"-" json:"-"`
466+
// Ready to use in SMTP responses.
467+
SMTPErrorCode int `sconf:"-" json:"-"`
468+
SMTPErrorSecode string `sconf:"-" json:"-"`
469+
SMTPErrorMsg string `sconf:"-" json:"-"`
465470
}
466471

467472
// Equal returns whether d and o are equal, only looking at their user-changeable fields.

config/doc.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,17 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
11201120
# Free-form comments. (optional)
11211121
Comment:
11221122
1123+
# If non-empty, incoming delivery attempts to this destination will be rejected
1124+
# during SMTP RCPT TO with this error response line. Useful when a catchall
1125+
# address is configured for the domain and messages to some addresses should be
1126+
# rejected. The response line must start with an error code. Currently the
1127+
# following error resonse codes are allowed: 421 (temporary local error), 550
1128+
# (user not found). If the line consists of only an error code, an appropriate
1129+
# error message is added. Rejecting messages with a 4xx code invites later retries
1130+
# by the remote, while 5xx codes should prevent further delivery attempts.
1131+
# (optional)
1132+
SMTPError:
1133+
11231134
# Full name to use in message From header when composing messages coming from this
11241135
# address with webmail. (optional)
11251136
FullName:

mox-/config.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,44 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
14361436

14371437
checkMailboxNormf(dest.Mailbox, "destination mailbox", addDestErrorf)
14381438

1439+
if dest.SMTPError != "" {
1440+
if len(dest.SMTPError) > 256 {
1441+
addDestErrorf("smtp error must be smaller than 256 bytes")
1442+
}
1443+
for _, c := range dest.SMTPError {
1444+
if c < ' ' || c >= 0x7f {
1445+
addDestErrorf("smtp error cannot contain contain control characters (including newlines) or non-ascii")
1446+
break
1447+
}
1448+
}
1449+
1450+
if dest.Mailbox != "" {
1451+
addDestErrorf("cannot have both SMTPError and Mailbox")
1452+
}
1453+
if len(dest.Rulesets) != 0 {
1454+
addDestErrorf("cannot have both SMTPError and Rulesets")
1455+
}
1456+
1457+
t := strings.SplitN(dest.SMTPError, " ", 2)
1458+
switch t[0] {
1459+
default:
1460+
addDestErrorf("smtp error must be 421 or 550 (with optional message), not %q", dest.SMTPError)
1461+
1462+
case "421":
1463+
dest.SMTPErrorCode = smtp.C451LocalErr
1464+
dest.SMTPErrorSecode = smtp.SeSys3Other0
1465+
dest.SMTPErrorMsg = "error processing"
1466+
case "550":
1467+
dest.SMTPErrorCode = smtp.C550MailboxUnavail
1468+
dest.SMTPErrorSecode = smtp.SeAddr1UnknownDestMailbox1
1469+
dest.SMTPErrorMsg = "no such user(s)"
1470+
}
1471+
if len(t) > 1 {
1472+
dest.SMTPErrorMsg = strings.TrimSpace(t[1])
1473+
}
1474+
acc.Destinations[addrName] = dest
1475+
}
1476+
14391477
for i, rs := range dest.Rulesets {
14401478
addRulesetErrorf := func(format string, args ...any) {
14411479
addDestErrorf("ruleset %d: %s", i+1, fmt.Sprintf(format, args...))

smtpserver/server.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,12 +1919,14 @@ func (c *conn) cmdRcpt(p *parser) {
19191919
xsmtpUserErrorf(smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, "not accepting email for ip")
19201920
}
19211921
c.recipients = append(c.recipients, recipient{fpath, nil, nil})
1922-
} else if accountName, alias, canonical, addr, err := mox.LookupAddress(fpath.Localpart, fpath.IPDomain.Domain, true, true); err == nil {
1922+
} else if accountName, alias, canonical, dest, err := mox.LookupAddress(fpath.Localpart, fpath.IPDomain.Domain, true, true); err == nil {
19231923
// note: a bare postmaster, without domain, is handled by LookupAddress. ../rfc/5321:735
19241924
if alias != nil {
19251925
c.recipients = append(c.recipients, recipient{fpath, nil, &rcptAlias{*alias, canonical}})
1926+
} else if dest.SMTPError != "" {
1927+
xsmtpServerErrorf(codes{dest.SMTPErrorCode, dest.SMTPErrorSecode}, dest.SMTPErrorMsg)
19261928
} else {
1927-
c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{accountName, addr, canonical}, nil})
1929+
c.recipients = append(c.recipients, recipient{fpath, &rcptAccount{accountName, dest, canonical}, nil})
19281930
}
19291931

19301932
} else if Localserve {

smtpserver/server_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2086,5 +2086,25 @@ test email
20862086
_, err = client.DeliverMultiple(ctxbg, mailFrom, rcptTo, int64(len(extraMsg)), strings.NewReader(extraMsg), true, true, false)
20872087
ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C554TransactionFailed, Secode: smtp.SeProto5TooManyRcpts3})
20882088
})
2089+
}
2090+
2091+
// TestDestinationSMTPError checks delivery to a destination with an SMTPError is rejected as configured.
2092+
func TestDestinationSMTPError(t *testing.T) {
2093+
resolver := dns.MockResolver{
2094+
A: map[string][]string{
2095+
"example.org.": {"127.0.0.10"}, // For mx check.
2096+
},
2097+
}
20892098

2099+
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
2100+
defer ts.close()
2101+
2102+
ts.run(func(err error, client *smtpclient.Client) {
2103+
t.Helper()
2104+
tcheck(t, err, "init client")
2105+
mailFrom := "[email protected]"
2106+
rcptTo := "[email protected]"
2107+
err = client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
2108+
ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
2109+
})
20902110
}

testdata/smtp/domains.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Accounts:
2424
# ohm sign, \u2126
2525
Ω@mox.example: nil
2626
27+
28+
SMTPError: 550 no more messages
2729
JunkFilter:
2830
Threshold: 0.9
2931
Params:

0 commit comments

Comments
 (0)