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 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-05-11 14:56:32 +02:00
parent 799c828548
commit 726dd78f3a
+53 -3
View File
@@ -12,6 +12,7 @@ import (
"io" "io"
"log/slog" "log/slog"
"net" "net"
"net/mail"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -77,7 +78,6 @@ func (d *Daemon) resolveTenantFromRcpts(rcpts []string) *int64 {
} }
ctx := context.Background() ctx := context.Background()
for _, rcpt := range rcpts { for _, rcpt := range rcpts {
// Strip angle brackets if present
addr := strings.Trim(rcpt, "<>") addr := strings.Trim(rcpt, "<>")
at := strings.LastIndex(addr, "@") at := strings.LastIndex(addr, "@")
if at < 0 { if at < 0 {
@@ -93,6 +93,54 @@ func (d *Daemon) resolveTenantFromRcpts(rcpts []string) *int64 {
return tenantID 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 return d.defaultTenantID
} }
@@ -272,8 +320,10 @@ func (s *session) Data(r io.Reader) error {
} }
raw := buf.Bytes() raw := buf.Bytes()
// Determine tenant from RCPT TO domain routing // Determine tenant: RCPT TO first, then header addresses (To/Cc/From).
tenantID := s.daemon.resolveTenantFromRcpts(s.rcpts) // 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) id, err := s.daemon.store.Save(context.Background(), raw, time.Now(), tenantID)
if err != nil { if err != nil {