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:
+53
-3
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user