Files
archivmail/internal/api/user_address_rules.go
patrickandClaude Sonnet 5.5 5248a177ce feat(PROJ-87,PROJ-90): Zusatzadressen pro User und Sichtbarkeit aller Mails eigener IMAP-Konten
Rolle user sieht zusätzlich Mails mit Zusatzadressen (PROJ-87) und alle Mails
von IMAP-Konten, deren owner dem User im selben Tenant entspricht (PROJ-90).
Liste, Nachprüfung und Detailzugriff nutzen dieselbe Regel (mailAccess).
Index: Token imapacct<id> in rcpt_addr, Reindex für Bestandsmails nötig.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 12:10:57 +01:00

77 lines
2.3 KiB
Go

package api
import (
"errors"
"net/http"
"net/mail"
"strings"
"unicode"
"archivmail/internal/auth"
"archivmail/internal/userstore"
)
const maxExtraAddressLen = 255
var errInvalidExtraAddress = errors.New("invalid address")
// Only bare, complete ASCII addresses: wildcards, display names, lists and
// bare domains would otherwise widen the Manticore phrase match.
func normalizeExtraAddress(raw string) (string, error) {
a := strings.ToLower(strings.TrimSpace(raw))
if a == "" || len(a) > maxExtraAddressLen {
return "", errInvalidExtraAddress
}
for _, r := range a {
if r <= ' ' || r > '~' || strings.ContainsRune(`*?%"<>()[]\,;:`, r) {
return "", errInvalidExtraAddress
}
}
local, domain, ok := strings.Cut(a, "@")
if !ok || local == "" || domain == "" || strings.Contains(domain, "@") {
return "", errInvalidExtraAddress
}
if !strings.ContainsFunc(local, isAlnum) || !strings.Contains(domain, ".") ||
strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") || strings.Contains(domain, "..") {
return "", errInvalidExtraAddress
}
parsed, err := mail.ParseAddress(a)
if err != nil || parsed.Name != "" || parsed.Address != a {
return "", errInvalidExtraAddress
}
return a, nil
}
func isAlnum(r rune) bool { return unicode.IsLetter(r) || unicode.IsDigit(r) }
// A tenant-less domain admin is refused instead of being treated as global.
func extraAddressRoleAllowed(sess *auth.Session) bool {
switch sess.Role {
case userstore.RoleSuperAdmin:
return true
case userstore.RoleDomainAdmin, userstore.RoleAdmin:
return sess.TenantID != nil
}
return false
}
// Returns 0 when allowed. Unknown users and users of other tenants both map
// to 404 so the existence of foreign user IDs is not revealed.
// Listing/removing stays possible after a role change so stored addresses
// can still be inspected and cleaned up; adding requires role "user".
func extraAddressTargetStatus(sess *auth.Session, target *userstore.User, adding bool) int {
if !extraAddressRoleAllowed(sess) {
return http.StatusForbidden
}
if target == nil {
return http.StatusNotFound
}
if sess.Role != userstore.RoleSuperAdmin && !tenantAccessAllowed(sess, target.TenantID) {
return http.StatusNotFound
}
if adding && target.Role != userstore.RoleUser {
return http.StatusBadRequest
}
return 0
}