按SMTP信封限定投递收件人并对齐通知配额
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-27 22:24:33 +08:00
parent 8c35d117b2
commit 67b33a28b9
5 changed files with 58 additions and 6 deletions

View File

@ -76,6 +76,7 @@ type Email struct {
InReplyTo string
ThreadReferences string
EnvelopeFrom string
EnvelopeTo []*User `json:"-"`
ReplyTo []*User
From *User
To []*User

View File

@ -50,7 +50,7 @@ func (s *Session) prepareNotification(email *parsemail.Email, rawBytes int) erro
return &smtp.SMTPError{Code: 550, EnhancedCode: smtp.EnhancedCode{5, 7, 1}, Message: "Notification recipient is not authorized by envelope"}
}
}
if err := s.authorization.ReserveNotification(len(recipients)); err != nil {
if err := s.authorization.ReserveNotification(len(s.To)); err != nil {
if errors.Is(err, notification.ErrQuota) {
return &smtp.SMTPError{Code: 452, EnhancedCode: smtp.EnhancedCode{4, 7, 0}, Message: "Notification sending quota exceeded"}
}

View File

@ -180,6 +180,10 @@ func (s *Session) Data(r io.Reader) (resultErr error) {
return send.SendTriggered(ctx, email, "系统通知 SMTP 发信")
}
}
email.EnvelopeTo = make([]*parsemail.User, len(s.To))
for index, address := range s.To {
email.EnvelopeTo[index] = &parsemail.User{EmailAddress: address}
}
resultErr = s.deliverOutgoingEmail(email, sender)
if resultErr == nil {
result = "accepted"

View File

@ -120,16 +120,23 @@ func sendRecipients(ctx *context.Context, e *parsemail.Email, recipients []*pars
return fmt.Errorf("build message: %w", err), nil
}
var to []*parsemail.User
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
if recipients != nil {
to = recipients
}
to := deliveryRecipients(e, recipients)
return doSend(ctx, fromDomain, b, to, e.From.EmailAddress, trigger)
}
func deliveryRecipients(e *parsemail.Email, recipients []*parsemail.User) []*parsemail.User {
if recipients != nil {
return recipients
}
if e.EnvelopeTo != nil {
return e.EnvelopeTo
}
var targets []*parsemail.User
return append(append(append(targets, e.To...), e.Cc...), e.Bcc...)
}
func parseRecipientAddress(user *parsemail.User) (string, string, error) {
if user == nil {
return "", "", errors.New("recipient is nil")

View File

@ -158,3 +158,43 @@ func TestDoSendRejectsEmptyRecipientList(t *testing.T) {
t.Fatalf("failure count = %d, want 1", len(failures))
}
}
func TestDeliveryRecipientsKeepsEnvelopeSeparateFromHeaders(t *testing.T) {
to := &parsemail.User{EmailAddress: "header-to@example.com"}
cc := &parsemail.User{EmailAddress: "header-cc@example.com"}
bcc := &parsemail.User{EmailAddress: "header-bcc@example.com"}
envelope := &parsemail.User{EmailAddress: "envelope@example.com"}
queued := &parsemail.User{EmailAddress: "queued@example.com"}
tests := []struct {
name string
envelope []*parsemail.User
override []*parsemail.User
want []*parsemail.User
}{
{name: "web message uses header recipients", want: []*parsemail.User{to, cc, bcc}},
{name: "smtp uses accepted envelope only", envelope: []*parsemail.User{envelope}, want: []*parsemail.User{envelope}},
{name: "empty envelope never falls back to headers", envelope: []*parsemail.User{}},
{name: "queue uses its single recipient", envelope: []*parsemail.User{envelope}, override: []*parsemail.User{queued}, want: []*parsemail.User{queued}},
{name: "explicit empty override never falls back", envelope: []*parsemail.User{envelope}, override: []*parsemail.User{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
message := &parsemail.Email{
To: []*parsemail.User{to}, Cc: []*parsemail.User{cc}, Bcc: []*parsemail.User{bcc},
EnvelopeTo: tt.envelope,
}
got := deliveryRecipients(message, tt.override)
if len(got) != len(tt.want) {
t.Fatalf("recipient count = %d, want %d", len(got), len(tt.want))
}
for index := range got {
if got[index] != tt.want[index] {
t.Fatalf("recipient %d = %v, want %v", index, got[index], tt.want[index])
}
}
if len(message.To) != 1 || message.To[0] != to || len(message.Cc) != 1 || message.Cc[0] != cc || len(message.Bcc) != 1 || message.Bcc[0] != bcc {
t.Fatal("delivery routing changed the message headers")
}
})
}
}