diff --git a/backend/dto/parsemail/email.go b/backend/dto/parsemail/email.go index 9f6aa33..3915905 100644 --- a/backend/dto/parsemail/email.go +++ b/backend/dto/parsemail/email.go @@ -76,6 +76,7 @@ type Email struct { InReplyTo string ThreadReferences string EnvelopeFrom string + EnvelopeTo []*User `json:"-"` ReplyTo []*User From *User To []*User diff --git a/backend/listen/smtp_server/notification.go b/backend/listen/smtp_server/notification.go index d48cae5..10b15ee 100644 --- a/backend/listen/smtp_server/notification.go +++ b/backend/listen/smtp_server/notification.go @@ -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"} } diff --git a/backend/listen/smtp_server/read_content.go b/backend/listen/smtp_server/read_content.go index 412b559..1f57b56 100644 --- a/backend/listen/smtp_server/read_content.go +++ b/backend/listen/smtp_server/read_content.go @@ -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" diff --git a/backend/utils/send/send.go b/backend/utils/send/send.go index 84699bb..f82dc43 100644 --- a/backend/utils/send/send.go +++ b/backend/utils/send/send.go @@ -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") diff --git a/backend/utils/send/send_test.go b/backend/utils/send/send_test.go index 73251d0..e750fb2 100644 --- a/backend/utils/send/send_test.go +++ b/backend/utils/send/send_test.go @@ -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") + } + }) + } +}