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>
77 lines
2.3 KiB
Go
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
|
|
}
|