feat(PROJ-9): implement labels backend - DB schema, labelstore, API handlers, Xapian integration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-18 08:32:30 +01:00
parent bd09992441
commit 5a6289c83d
8 changed files with 995 additions and 2 deletions
+44
View File
@@ -25,6 +25,7 @@ import (
"github.com/archivmail/internal/auth"
imapstore "github.com/archivmail/internal/imap"
"github.com/archivmail/internal/index"
"github.com/archivmail/internal/labelstore"
ldapcfg "github.com/archivmail/internal/ldapconfig"
pop3store "github.com/archivmail/internal/pop3"
"github.com/archivmail/internal/smtpd"
@@ -79,6 +80,7 @@ type Server struct {
pop3Store *pop3store.Store
pop3Importer *pop3store.Importer
uploadJobs sync.Map // jobID → *UploadJob
labels *labelstore.Store // PROJ-9: label management
ldapStore *ldapcfg.Store
tenantStore *tenantstore.Store
tenantLdapStore *ldapcfg.TenantStore // PROJ-23: per-tenant LDAP config
@@ -600,6 +602,7 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
dateToStr := r.URL.Query().Get("date_to")
sortParam := r.URL.Query().Get("sort") // "relevance", "date_asc", "date_desc"
hasAttachStr := r.URL.Query().Get("has_attachment") // "true" or "false"
labelIDStr := r.URL.Query().Get("label_id") // PROJ-9: filter by label
pageStr := r.URL.Query().Get("page")
pageSizeStr := r.URL.Query().Get("page_size")
@@ -616,6 +619,13 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
Page: page,
}
// PROJ-9: Parse label_id filter.
if labelIDStr != "" {
if lid, err := strconv.ParseInt(labelIDStr, 10, 64); err == nil && lid > 0 {
req.LabelID = &lid
}
}
if hasAttachStr == "true" {
v := true
req.HasAttachment = &v
@@ -691,6 +701,25 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
}
}
// PROJ-9: Post-filter by label_id when the label store is available.
if req.LabelID != nil && s.labels != nil && len(result.Hits) > 0 {
labelEmailIDs, lErr := s.labels.GetEmailIDsByLabel(r.Context(), *req.LabelID)
if lErr == nil {
allowed := make(map[string]struct{}, len(labelEmailIDs))
for _, id := range labelEmailIDs {
allowed[id] = struct{}{}
}
filtered := result.Hits[:0]
for _, h := range result.Hits {
if _, ok := allowed[h.ID]; ok {
filtered = append(filtered, h)
}
}
result.Hits = filtered
result.Total = len(filtered)
}
}
sess := sessionFromCtx(r.Context())
s.audlog.Log(audit.Entry{
EventType: audit.EventSearch,
@@ -710,7 +739,19 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
Date string `json:"date,omitempty"`
Size int64 `json:"size,omitempty"`
HasAttachments bool `json:"has_attachments"`
LabelIDs []int64 `json:"label_ids,omitempty"` // PROJ-9
}
// PROJ-9: Batch-load label IDs for all hits.
var labelMap map[string][]int64
if s.labels != nil && len(result.Hits) > 0 {
emailIDs := make([]string, len(result.Hits))
for i, h := range result.Hits {
emailIDs[i] = h.ID
}
labelMap, _ = s.labels.GetLabelsForEmails(r.Context(), emailIDs)
}
enriched := make([]enrichedHit, 0, len(result.Hits))
for _, h := range result.Hits {
eh := enrichedHit{ID: h.ID, Score: h.Score}
@@ -728,6 +769,9 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
eh.HasAttachments = len(pm.Attachments) > 0
}
}
if labelMap != nil {
eh.LabelIDs = labelMap[h.ID]
}
enriched = append(enriched, eh)
}