拒绝匿名外域收件并补充逐端口投递诊断
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-10-02 13:52:37 +08:00
parent 7612e1002b
commit 9e78499a65
3 changed files with 69 additions and 2 deletions

View File

@ -107,6 +107,27 @@ func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error {
if err := s.checkAuthorization(); err != nil {
return err
}
if !validation.IsMailboxAddress(to) {
return &smtp.SMTPError{Code: 550, EnhancedCode: smtp.EnhancedCode{5, 1, 3}, Message: "Invalid recipient address"}
}
// 匿名连接仅接收受管域名的邮件,不能等到正文阶段再静默丢弃外域收件人。
if s.authorization == nil {
domains := config.CurrentMailDomains().Accepted
if len(domains) == 0 {
return &smtp.SMTPError{Code: 451, EnhancedCode: smtp.EnhancedCode{4, 3, 0}, Message: "Mail domains are unavailable"}
}
domain := to[strings.LastIndex(to, "@")+1:]
local := false
for _, accepted := range domains {
if strings.EqualFold(domain, accepted) {
local = true
break
}
}
if !local {
return &smtp.SMTPError{Code: 550, EnhancedCode: smtp.EnhancedCode{5, 7, 1}, Message: "Relay access denied"}
}
}
if validation.IsNotificationAddress(to, config.CurrentMailDomains().Accepted) {
return &smtp.SMTPError{Code: 550, EnhancedCode: smtp.EnhancedCode{5, 1, 1}, Message: "Notification address does not accept mail"}
}

View File

@ -5,10 +5,44 @@ import (
"strings"
"testing"
"git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/utils/context"
"github.com/emersion/go-smtp"
)
func TestAnonymousRcptRejectsForeignDomains(t *testing.T) {
oldConfig := config.Instance
t.Cleanup(func() { config.Instance = oldConfig })
config.Instance = &config.Config{Domain: "example.com", Domains: []string{"example.com", "alias.example"}}
for _, tt := range []struct {
address string
allowed bool
}{
{"alice@example.com", true},
{"alice@EXAMPLE.COM", true},
{"alice@alias.example", true},
{"alice@external.example", false},
{"alice@example.com.attacker.test", false},
{"alice@sub.example.com", false},
{"not-an-address", false},
} {
t.Run(tt.address, func(t *testing.T) {
session := &Session{Ctx: &context.Context{}, To: []string{"existing@example.com"}}
err := session.Rcpt(tt.address, nil)
if tt.allowed {
if err != nil || len(session.To) != 2 {
t.Fatalf("受管域名收件失败:%v", err)
}
return
}
var denied *smtp.SMTPError
if !errors.As(err, &denied) || denied.Code != 550 || len(session.To) != 1 {
t.Fatalf("匿名外域收件未明确拒绝或污染收件列表:%v", err)
}
})
}
}
func TestSessionResetClearsTransactionStateOnly(t *testing.T) {
ctx := &context.Context{UserID: 42, UserAccount: "smail", IsAdmin: true}
session := &Session{

View File

@ -287,10 +287,22 @@ func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemai
}
err = deliverMXContext(deliveryContext, hosts, func(host, port string) error {
address := net.JoinHostPort(host, port)
var attemptErr error
if port == "465" {
return smtp.SendMailWithTLSContext(deliveryContext, host, address, nil, from, fromDomain, buildAddress(tos), data)
attemptErr = smtp.SendMailWithTLSContext(deliveryContext, host, address, nil, from, fromDomain, buildAddress(tos), data)
} else {
attemptErr = smtp.SendMailContext(deliveryContext, host, address, nil, from, fromDomain, buildAddress(tos), data)
}
return smtp.SendMailContext(deliveryContext, host, address, nil, from, fromDomain, buildAddress(tos), data)
if attemptErr != nil {
// 分端口记录网络失败,不记录邮件内容、凭据及远端 SMTP 响应原文。
fields := log.Fields{"action": "外域邮件投递尝试", "mxHost": host, "mxPort": port, "reason": logstore.MailFailureReason(attemptErr)}
var networkErr *net.OpError
if errors.As(attemptErr, &networkErr) {
fields["networkError"] = networkErr.Error()
}
log.WithContext(ctx).WithFields(fields).Warn("外域邮件投递通道失败")
}
return attemptErr
})
if err != nil {
recordFailure(err)