feat(PROJ-30): Xapian → Manticore Search Migration
- internal/index/manticore.go: ManticoreTenantManager + manticoreIndex (RT-Indizes, CGO-frei) - internal/index/index.go: TenantIndexer Interface (Xapian + Manticore) - internal/index/tenant_worker.go: mgr-Typ auf TenantIndexer Interface - internal/api/server.go: idxMgr auf TenantIndexer Interface - config/config.go: IndexConfig.ManticoreDSN Feld - cmd/archivmail/cmd_reindex.go: reindex Subkommando - cmd/archivmail/main.go: Manticore-Branch + reindex Case - go.mod: github.com/go-sql-driver/mysql v1.8.1 - update.sh: Manticore auto-install, CGO_ENABLED=0, config.yml migration, auto-reindex fix(IMAP): TCP-Deadline-Wrapper für steckengebliebene Imports fix(auth): Email-Claim in JWT für User-Isolation fix(search): User-Isolation via sess.Email (fail-safe) fix(ui): Admin-Login Auth-Cache, Logout-Redirect, IMAP-Polling-Resilienz Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -171,6 +171,18 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
labelMap, _ = s.labels.GetLabelsForEmails(r.Context(), emailIDs)
|
||||
}
|
||||
|
||||
// SEC: For user role, restrict results to mails the user is involved in
|
||||
// (From, To, or CC). Email comes from the JWT session — no DB lookup needed.
|
||||
// If email is missing for a user-role session, block all results (fail-safe).
|
||||
var userEmailFilter string
|
||||
if sess.Role == userstore.RoleUser {
|
||||
userEmailFilter = strings.ToLower(sess.Email)
|
||||
if userEmailFilter == "" {
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"total": 0, "hits": []interface{}{}})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
enriched := make([]enrichedHit, 0, len(result.Hits))
|
||||
for _, h := range result.Hits {
|
||||
eh := enrichedHit{ID: h.ID, Score: h.Score}
|
||||
@@ -186,6 +198,14 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
eh.Date = pm.Date.UTC().Format(time.RFC3339)
|
||||
}
|
||||
eh.HasAttachments = len(pm.Attachments) > 0
|
||||
|
||||
// User isolation: skip mails the user is not involved in.
|
||||
if userEmailFilter != "" && !mailBelongsToUser(pm, userEmailFilter) {
|
||||
continue
|
||||
}
|
||||
} else if userEmailFilter != "" {
|
||||
// If mail can't be parsed, deny access to user role.
|
||||
continue
|
||||
}
|
||||
}
|
||||
if labelMap != nil {
|
||||
@@ -233,8 +253,7 @@ func (s *Server) handleGetMail(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// user and auditor: only own mails; domain_auditor: all tenant mails (no filter)
|
||||
if sess.Role == userstore.RoleUser || sess.Role == userstore.RoleAuditor {
|
||||
u, err := s.users.GetByUsername(sess.Username)
|
||||
if err != nil || !mailBelongsToUser(pm, u.Email) {
|
||||
if sess.Email == "" || !mailBelongsToUser(pm, sess.Email) {
|
||||
writeError(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
@@ -399,16 +418,21 @@ func (s *Server) handleGetRaw(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(raw)
|
||||
}
|
||||
|
||||
// mailBelongsToUser checks if the user's email appears in To or CC.
|
||||
// mailBelongsToUser checks if the user's email appears in From, To, or CC.
|
||||
// Users can access mails they sent as well as mails they received.
|
||||
// From may contain a display name ("Name <addr>"), so Contains is used.
|
||||
func mailBelongsToUser(pm *mailparser.ParsedMail, userEmail string) bool {
|
||||
email := strings.ToLower(userEmail)
|
||||
if strings.Contains(strings.ToLower(pm.From), email) {
|
||||
return true
|
||||
}
|
||||
for _, to := range pm.To {
|
||||
if strings.ToLower(to) == email {
|
||||
if strings.Contains(strings.ToLower(to), email) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, cc := range pm.CC {
|
||||
if strings.ToLower(cc) == email {
|
||||
if strings.Contains(strings.ToLower(cc), email) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user