From 726dd78f3ae98c5ef5f0d2083f6eec334a677654 Mon Sep 17 00:00:00 2001 From: sysops Date: Mon, 11 May 2026 14:56:32 +0200 Subject: [PATCH] fix(PROJ-4): SMTP tenant routing: header fallback for BCC journaling resolveTenant() now tries RCPT TO first, then falls back to parsing To/Cc/From headers. Needed because BCC-journaled mails arrive with RCPT TO = the archive's own BCC address, not the real recipient's domain. Co-Authored-By: Claude Sonnet 4.6 --- internal/smtpd/smtpd.go | 56 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/internal/smtpd/smtpd.go b/internal/smtpd/smtpd.go index 7007bc8..8dfcb35 100644 --- a/internal/smtpd/smtpd.go +++ b/internal/smtpd/smtpd.go @@ -12,6 +12,7 @@ import ( "io" "log/slog" "net" + "net/mail" "strings" "sync" "sync/atomic" @@ -77,7 +78,6 @@ func (d *Daemon) resolveTenantFromRcpts(rcpts []string) *int64 { } ctx := context.Background() for _, rcpt := range rcpts { - // Strip angle brackets if present addr := strings.Trim(rcpt, "<>") at := strings.LastIndex(addr, "@") if at < 0 { @@ -93,6 +93,54 @@ func (d *Daemon) resolveTenantFromRcpts(rcpts []string) *int64 { return tenantID } } + return nil +} + +// resolveTenant resolves the tenant for an incoming mail. +// Priority: SMTP RCPT TO → header addresses (To, Cc, From) → defaultTenantID. +// This handles BCC-journaling where RCPT TO is the archive's own address and +// the real sender/recipient domain is only visible in the RFC 2822 headers. +func (d *Daemon) resolveTenant(rcpts []string, raw []byte) *int64 { + if d.domainToTenant == nil { + return d.defaultTenantID + } + + // 1. Envelope RCPT TO + if tid := d.resolveTenantFromRcpts(rcpts); tid != nil { + return tid + } + + // 2. Header addresses: To, Cc, From + msg, err := mail.ReadMessage(bytes.NewReader(raw)) + if err == nil { + ctx := context.Background() + for _, hdr := range []string{"To", "Cc", "From"} { + val := msg.Header.Get(hdr) + if val == "" { + continue + } + addrs, err := mail.ParseAddressList(val) + if err != nil { + continue + } + for _, a := range addrs { + at := strings.LastIndex(a.Address, "@") + if at < 0 { + continue + } + domain := strings.ToLower(a.Address[at+1:]) + tid, err := d.domainToTenant(ctx, domain) + if err != nil { + d.logger.Warn("SMTP: tenant lookup failed", "domain", domain, "err", err) + continue + } + if tid != nil { + return tid + } + } + } + } + return d.defaultTenantID } @@ -272,8 +320,10 @@ func (s *session) Data(r io.Reader) error { } raw := buf.Bytes() - // Determine tenant from RCPT TO domain routing - tenantID := s.daemon.resolveTenantFromRcpts(s.rcpts) + // Determine tenant: RCPT TO first, then header addresses (To/Cc/From). + // The header fallback is needed for BCC journaling where RCPT TO is the + // archive's own address, not the real recipient. + tenantID := s.daemon.resolveTenant(s.rcpts, raw) id, err := s.daemon.store.Save(context.Background(), raw, time.Now(), tenantID) if err != nil {