zwischenstand

This commit is contained in:
Christoph K.
2026-03-20 23:24:56 +01:00
parent b1a576f61e
commit 905981cd1e
25 changed files with 3607 additions and 217 deletions

View File

@@ -12,6 +12,30 @@ import (
"gopkg.in/yaml.v3"
)
// ArchiveFolder beschreibt einen IMAP-Archivordner mit optionaler Aufbewahrungsdauer.
type ArchiveFolder struct {
Name string `yaml:"name"` // Anzeigename (z.B. "5Jahre")
IMAPFolder string `yaml:"imap_folder"` // Echter IMAP-Ordnername (z.B. "5Jahre")
RetentionDays int `yaml:"retention_days"` // 0 = dauerhaft behalten
}
// EmailAccount beschreibt einen einzelnen IMAP-Account.
type EmailAccount struct {
Name string `yaml:"name"`
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
TLS bool `yaml:"tls"`
StartTLS bool `yaml:"starttls"`
Folder string `yaml:"folder"`
ProcessedFolder string `yaml:"processed_folder"`
Model string `yaml:"model"`
ArchiveFolders []ArchiveFolder `yaml:"archive_folders"`
TriageImportantFolder string `yaml:"triage_important_folder"` // Ordner für wichtige Emails (leer = in INBOX lassen)
TriageUnimportantFolder string `yaml:"triage_unimportant_folder"` // Ordner für unwichtige Emails (leer = kein Triage)
}
type Config struct {
Qdrant struct {
Host string `yaml:"host"`
@@ -32,22 +56,30 @@ type Config struct {
} `yaml:"chat"`
Discord struct {
Token string `yaml:"token"`
GuildID string `yaml:"guild_id"`
Token string `yaml:"token"`
GuildID string `yaml:"guild_id"`
AllowedUsers []string `yaml:"allowed_users"` // Wenn gesetzt, dürfen nur diese User-IDs den Bot nutzen
} `yaml:"discord"`
// Email ist der Legacy-Block für einen einzelnen Account.
Email struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
TLS bool `yaml:"tls"`
StartTLS bool `yaml:"starttls"`
Folder string `yaml:"folder"`
ProcessedFolder string `yaml:"processed_folder"` // Zielordner nach Zusammenfassung (leer = kein Verschieben)
Model string `yaml:"model"` // Optional: überschreibt chat.model für Email-Zusammenfassungen
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
TLS bool `yaml:"tls"`
StartTLS bool `yaml:"starttls"`
Folder string `yaml:"folder"`
ProcessedFolder string `yaml:"processed_folder"`
Model string `yaml:"model"`
ArchiveFolders []ArchiveFolder `yaml:"archive_folders"`
TriageImportantFolder string `yaml:"triage_important_folder"`
TriageUnimportantFolder string `yaml:"triage_unimportant_folder"`
} `yaml:"email"`
// EmailAccounts ermöglicht mehrere IMAP-Accounts. Hat Vorrang vor email:.
EmailAccounts []EmailAccount `yaml:"email_accounts"`
Tasks struct {
StorePath string `yaml:"store_path"`
} `yaml:"tasks"`
@@ -56,15 +88,52 @@ type Config struct {
ChannelID string `yaml:"channel_id"`
EmailIntervalMin int `yaml:"email_interval_min"`
TaskReminderHour int `yaml:"task_reminder_hour"`
CleanupHour int `yaml:"cleanup_hour"` // Uhrzeit für tägliches Archiv-Aufräumen (Standard: 2)
IngestHour int `yaml:"ingest_hour"` // Uhrzeit für nächtlichen Email-Ingest (Standard: 23, 0 = deaktiviert)
} `yaml:"daemon"`
BrainRoot string `yaml:"brain_root"`
TopK uint64 `yaml:"top_k"`
ScoreThreshold float32 `yaml:"score_threshold"`
// RSSFeeds definiert RSS-Feeds die automatisch überwacht werden.
RSSFeeds []RSSFeed `yaml:"rss_feeds"`
}
// RSSFeed beschreibt einen RSS-Feed mit Polling-Intervall.
type RSSFeed struct {
URL string `yaml:"url"`
IntervalHours int `yaml:"interval_hours"` // 0 = Standard 24h
}
var Cfg Config
// AllEmailAccounts gibt alle konfigurierten Email-Accounts zurück.
// Wenn email_accounts konfiguriert ist, hat das Vorrang vor dem Legacy-email:-Block.
func AllEmailAccounts() []EmailAccount {
if len(Cfg.EmailAccounts) > 0 {
return Cfg.EmailAccounts
}
if Cfg.Email.Host == "" {
return nil
}
return []EmailAccount{{
Name: "Email",
Host: Cfg.Email.Host,
Port: Cfg.Email.Port,
User: Cfg.Email.User,
Password: Cfg.Email.Password,
TLS: Cfg.Email.TLS,
StartTLS: Cfg.Email.StartTLS,
Folder: Cfg.Email.Folder,
ProcessedFolder: Cfg.Email.ProcessedFolder,
Model: Cfg.Email.Model,
ArchiveFolders: Cfg.Email.ArchiveFolders,
TriageImportantFolder: Cfg.Email.TriageImportantFolder,
TriageUnimportantFolder: Cfg.Email.TriageUnimportantFolder,
}}
}
// NewQdrantConn öffnet eine gRPC-Verbindung zur Qdrant-Instanz.
// Der Aufrufer ist verantwortlich für conn.Close().
func NewQdrantConn() *grpc.ClientConn {

View File

@@ -0,0 +1,87 @@
package config
import "testing"
func TestAllEmailAccounts_Empty(t *testing.T) {
orig := Cfg
defer func() { Cfg = orig }()
Cfg = Config{}
accounts := AllEmailAccounts()
if len(accounts) != 0 {
t.Errorf("erwartet 0 Accounts, got %d", len(accounts))
}
}
func TestAllEmailAccounts_LegacyFallback(t *testing.T) {
orig := Cfg
defer func() { Cfg = orig }()
Cfg = Config{}
Cfg.Email.Host = "imap.example.de"
Cfg.Email.Port = 143
Cfg.Email.User = "user@example.de"
Cfg.Email.Password = "geheim"
Cfg.Email.Folder = "INBOX"
Cfg.Email.ProcessedFolder = "Processed"
Cfg.Email.Model = "testmodel"
accounts := AllEmailAccounts()
if len(accounts) != 1 {
t.Fatalf("erwartet 1 Account, got %d", len(accounts))
}
a := accounts[0]
if a.Host != "imap.example.de" {
t.Errorf("Host: got %q", a.Host)
}
if a.Port != 143 {
t.Errorf("Port: got %d", a.Port)
}
if a.User != "user@example.de" {
t.Errorf("User: got %q", a.User)
}
if a.ProcessedFolder != "Processed" {
t.Errorf("ProcessedFolder: got %q", a.ProcessedFolder)
}
if a.Model != "testmodel" {
t.Errorf("Model: got %q", a.Model)
}
}
func TestAllEmailAccounts_MultipleAccounts(t *testing.T) {
orig := Cfg
defer func() { Cfg = orig }()
Cfg = Config{}
Cfg.EmailAccounts = []EmailAccount{
{Name: "Privat", Host: "imap1.de", Port: 143},
{Name: "Arbeit", Host: "imap2.de", Port: 993, TLS: true},
}
accounts := AllEmailAccounts()
if len(accounts) != 2 {
t.Fatalf("erwartet 2 Accounts, got %d", len(accounts))
}
if accounts[0].Host != "imap1.de" {
t.Errorf("Account 0 Host: got %q", accounts[0].Host)
}
if accounts[1].Host != "imap2.de" {
t.Errorf("Account 1 Host: got %q", accounts[1].Host)
}
}
func TestAllEmailAccounts_NewTakesPrecedence(t *testing.T) {
orig := Cfg
defer func() { Cfg = orig }()
Cfg = Config{}
Cfg.Email.Host = "legacy.de"
Cfg.EmailAccounts = []EmailAccount{
{Name: "Neu", Host: "new.de"},
}
accounts := AllEmailAccounts()
if len(accounts) != 1 {
t.Fatalf("erwartet 1 Account, got %d", len(accounts))
}
if accounts[0].Host != "new.de" {
t.Errorf("email_accounts sollte Vorrang haben, got host %q", accounts[0].Host)
}
}