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:
sysops
2026-04-03 21:19:36 +02:00
parent e90d588e30
commit a93a843506
19 changed files with 742 additions and 65 deletions
+62 -16
View File
@@ -3,17 +3,35 @@ package imap
import (
"crypto/tls"
"fmt"
"net"
"strings"
"time"
imapv2 "github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapclient"
)
// FolderInfo describes a single IMAP folder with exclusion metadata.
type FolderInfo struct {
Name string `json:"name"`
Excluded bool `json:"excluded"`
Reason string `json:"reason,omitempty"`
const (
dialTimeout = 30 * time.Second
fetchTimeout = 5 * time.Minute // per-batch read/write deadline
)
// Conn wraps an IMAP client with the underlying net.Conn so callers
// can set per-operation deadlines to prevent indefinite blocking.
type Conn struct {
*imapclient.Client
raw net.Conn
}
// SetFetchDeadline sets a 5-minute read/write deadline on the connection.
// Call this before each fetch batch to prevent stalled imports.
func (c *Conn) SetFetchDeadline() {
_ = c.raw.SetDeadline(time.Now().Add(fetchTimeout))
}
// ClearDeadline removes any active deadline from the underlying connection.
func (c *Conn) ClearDeadline() {
_ = c.raw.SetDeadline(time.Time{})
}
// junkTrashNames lists well-known junk/trash folder names for fallback detection.
@@ -22,33 +40,61 @@ var junkTrashNames = []string{
"deleted messages", "papierkorb", "gelöschte elemente",
}
// FolderInfo describes a single IMAP folder with exclusion metadata.
type FolderInfo struct {
Name string `json:"name"`
Excluded bool `json:"excluded"`
Reason string `json:"reason,omitempty"`
}
// Connect establishes an IMAP client connection using the specified TLS mode.
func Connect(host string, port int, tlsMode string) (*imapclient.Client, error) {
// Returns a Conn that exposes the underlying net.Conn for deadline management.
func Connect(host string, port int, tlsMode string) (*Conn, error) {
addr := fmt.Sprintf("%s:%d", host, port)
switch tlsMode {
case "ssl":
c, err := imapclient.DialTLS(addr, &imapclient.Options{
TLSConfig: &tls.Config{ServerName: host},
})
dialer := &tls.Dialer{
NetDialer: &net.Dialer{Timeout: dialTimeout},
Config: &tls.Config{ServerName: host},
}
raw, err := dialer.Dial("tcp", addr)
if err != nil {
return nil, fmt.Errorf("imap connect ssl: %w", err)
}
return c, nil
c, err := imapclient.New(raw, nil)
if err != nil {
raw.Close()
return nil, fmt.Errorf("imap client ssl: %w", err)
}
return &Conn{Client: c, raw: raw}, nil
case "starttls":
c, err := imapclient.DialStartTLS(addr, &imapclient.Options{
TLSConfig: &tls.Config{ServerName: host},
})
raw, err := net.DialTimeout("tcp", addr, dialTimeout)
if err != nil {
return nil, fmt.Errorf("imap connect starttls: %w", err)
}
return c, nil
c, err := imapclient.New(raw, &imapclient.Options{
TLSConfig: &tls.Config{ServerName: host},
})
if err != nil {
raw.Close()
return nil, fmt.Errorf("imap client starttls: %w", err)
}
return &Conn{Client: c, raw: raw}, nil
case "none":
c, err := imapclient.DialInsecure(addr, nil)
raw, err := net.DialTimeout("tcp", addr, dialTimeout)
if err != nil {
return nil, fmt.Errorf("imap connect plain: %w", err)
}
return c, nil
c, err := imapclient.New(raw, nil)
if err != nil {
raw.Close()
return nil, fmt.Errorf("imap client plain: %w", err)
}
return &Conn{Client: c, raw: raw}, nil
default:
return nil, fmt.Errorf("imap: unknown tls mode %q", tlsMode)
}