feat(PROJ-46): E-Mail als primärer Login-Identifier für Tenant-User
Tenant-User (tenant_id IS NOT NULL) melden sich künftig per E-Mail an statt per Username — behebt Verwechslungen wie im Support-Fall vom 2026-06-13 (Login schlug trotz Passwort-Reset fehl, weil E-Mail statt Username verwendet wurde). Nicht-Tenant-User (Superadmin/System) können weiterhin Username ODER E-Mail nutzen. Neue Store.VerifyLogin() prüft erst per E-Mail (alle User), fällt dann auf Username zurück (nur tenant_id IS NULL). VerifyPassword() bleibt für den IMAP-Server-Login-Pfad (PROJ-26) unverändert. Bewusster Breaking Change für Tenant-User, Datenqualität vorab geprüft (0 Kollisionen). Security-Nachtrag: bcrypt-Dummy-Compare im "user not found"-Pfad ergänzt, um Timing-basierte Identifier-Enumeration zu verhindern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
804cd62201
commit
767373b206
@@ -101,6 +101,13 @@ func (d *Daemon) resolveTenantFromRcpts(rcpts []string) *int64 {
|
||||
// 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 {
|
||||
// 0. PROJ-43: explicit pattern routing rules take precedence over the plain
|
||||
// tenant_domains mapping, because they are more specific (wildcard
|
||||
// domains, exact sender/recipient addresses, priority ordering).
|
||||
if tid := d.resolveTenantByRules(rcpts, raw); tid != nil {
|
||||
return tid
|
||||
}
|
||||
|
||||
if d.domainToTenant == nil {
|
||||
return d.defaultTenantID
|
||||
}
|
||||
@@ -144,6 +151,38 @@ func (d *Daemon) resolveTenant(rcpts []string, raw []byte) *int64 {
|
||||
return d.defaultTenantID
|
||||
}
|
||||
|
||||
// resolveTenantByRules gathers the From address and recipient list (envelope
|
||||
// RCPT TO plus header To/Cc) and evaluates the PROJ-43 tenant_routing_rules.
|
||||
// Returns nil when the store is unavailable or no rule matches.
|
||||
func (d *Daemon) resolveTenantByRules(rcpts []string, raw []byte) *int64 {
|
||||
if d.store == nil {
|
||||
return nil
|
||||
}
|
||||
var from string
|
||||
recipients := make([]string, 0, len(rcpts)+4)
|
||||
for _, r := range rcpts {
|
||||
recipients = append(recipients, strings.Trim(r, "<>"))
|
||||
}
|
||||
if msg, err := mail.ReadMessage(bytes.NewReader(raw)); err == nil {
|
||||
if addrs, err := mail.ParseAddressList(msg.Header.Get("From")); err == nil && len(addrs) > 0 {
|
||||
from = addrs[0].Address
|
||||
}
|
||||
for _, hdr := range []string{"To", "Cc"} {
|
||||
if addrs, err := mail.ParseAddressList(msg.Header.Get(hdr)); err == nil {
|
||||
for _, a := range addrs {
|
||||
recipients = append(recipients, a.Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tid, err := d.store.ResolveTenantByRoutingRules(context.Background(), from, recipients)
|
||||
if err != nil {
|
||||
d.logger.Warn("SMTP: routing rule lookup failed", "err", err)
|
||||
return nil
|
||||
}
|
||||
return tid
|
||||
}
|
||||
|
||||
// SetIndexCallback sets the function called after each successfully stored mail.
|
||||
func (d *Daemon) SetIndexCallback(cb IndexCallback) {
|
||||
d.indexCallback = cb
|
||||
|
||||
Reference in New Issue
Block a user