zwischenstand
This commit is contained in:
@@ -11,60 +11,510 @@ import (
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
|
||||
"my-brain-importer/internal/config"
|
||||
"my-brain-importer/internal/triage"
|
||||
)
|
||||
|
||||
// Summarize verbindet sich mit IMAP, holt die letzten 20 Emails und fasst sie per LLM zusammen.
|
||||
func Summarize() (string, error) {
|
||||
return fetchAndSummarize(20, "Fasse diese Emails kurz zusammen und hebe wichtige oder dringende hervor.")
|
||||
accounts := config.AllEmailAccounts()
|
||||
if len(accounts) == 0 {
|
||||
return "", fmt.Errorf("Kein Email-Account konfiguriert")
|
||||
}
|
||||
if len(accounts) == 1 {
|
||||
return fetchAndSummarizeAccount(accounts[0], 20, "Fasse diese Emails kurz zusammen und hebe wichtige oder dringende hervor.")
|
||||
}
|
||||
var parts []string
|
||||
for _, acc := range accounts {
|
||||
result, err := fetchAndSummarizeAccount(acc, 20, "Fasse diese Emails kurz zusammen und hebe wichtige oder dringende hervor.")
|
||||
if err != nil {
|
||||
slog.Error("Email-Fehler", "account", acc.Name, "fehler", err)
|
||||
parts = append(parts, fmt.Sprintf("❌ **%s:** %v", accountLabel(acc), err))
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("**%s:**\n%s", accountLabel(acc), result))
|
||||
}
|
||||
return strings.Join(parts, "\n\n"), nil
|
||||
}
|
||||
|
||||
// SummarizeUnread fasst ungelesene Emails zusammen.
|
||||
// Wenn email.processed_folder konfiguriert ist, werden die Emails danach dorthin verschoben.
|
||||
// SummarizeUnread fasst ungelesene Emails für alle konfigurierten Accounts zusammen.
|
||||
// Wenn email.processed_folder konfiguriert ist, werden Emails danach dorthin verschoben.
|
||||
func SummarizeUnread() (string, error) {
|
||||
cl, err := Connect()
|
||||
accounts := config.AllEmailAccounts()
|
||||
if len(accounts) == 0 {
|
||||
return "", fmt.Errorf("Kein Email-Account konfiguriert")
|
||||
}
|
||||
if len(accounts) == 1 {
|
||||
return SummarizeUnreadAccount(accounts[0])
|
||||
}
|
||||
|
||||
var parts []string
|
||||
allEmpty := true
|
||||
for _, acc := range accounts {
|
||||
result, err := SummarizeUnreadAccount(acc)
|
||||
if err != nil {
|
||||
slog.Error("Email-Fehler", "account", acc.Name, "fehler", err)
|
||||
parts = append(parts, fmt.Sprintf("❌ **%s:** %v", accountLabel(acc), err))
|
||||
continue
|
||||
}
|
||||
if result != "📭 Keine ungelesenen Emails." {
|
||||
allEmpty = false
|
||||
parts = append(parts, fmt.Sprintf("**%s:**\n%s", accountLabel(acc), result))
|
||||
}
|
||||
}
|
||||
if allEmpty {
|
||||
return "📭 Keine ungelesenen Emails.", nil
|
||||
}
|
||||
return strings.Join(parts, "\n\n"), nil
|
||||
}
|
||||
|
||||
// SummarizeUnreadAccount fasst ungelesene Emails für einen bestimmten Account zusammen.
|
||||
// Wenn triage_folder konfiguriert ist, werden unwichtige Emails vorher aussortiert.
|
||||
func SummarizeUnreadAccount(acc config.EmailAccount) (string, error) {
|
||||
// Phase 1: Triage – Emails sortieren (eigene Verbindung)
|
||||
if acc.TriageUnimportantFolder != "" || acc.TriageImportantFolder != "" {
|
||||
if err := triageUnread(acc); err != nil {
|
||||
slog.Warn("[Triage] fehlgeschlagen, übersprungen", "account", accountLabel(acc), "fehler", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Zusammenfassung der verbleibenden wichtigen Emails (frische Verbindung)
|
||||
cl, err := ConnectAccount(acc)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Email-Verbindung: %w", err)
|
||||
return "", fmt.Errorf("Email-Verbindung (%s): %w", accountLabel(acc), err)
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
processedFolder := config.Cfg.Email.ProcessedFolder
|
||||
|
||||
var msgs []Message
|
||||
var seqNums []uint32
|
||||
|
||||
if processedFolder != "" {
|
||||
if acc.ProcessedFolder != "" {
|
||||
msgs, seqNums, err = cl.FetchUnreadSeqNums()
|
||||
} else {
|
||||
msgs, err = cl.FetchUnread()
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Emails abrufen: %w", err)
|
||||
return "", fmt.Errorf("Emails abrufen (%s): %w", accountLabel(acc), err)
|
||||
}
|
||||
if len(msgs) == 0 {
|
||||
return "📭 Keine ungelesenen Emails.", nil
|
||||
}
|
||||
|
||||
slog.Info("Email-Zusammenfassung gestartet", "anzahl", len(msgs), "typ", "unread")
|
||||
result, err := summarizeWithLLM(msgs, "Fasse diese ungelesenen Emails zusammen. Hebe wichtige, dringende oder actionable Emails hervor.")
|
||||
slog.Info("Email-Zusammenfassung gestartet", "account", accountLabel(acc), "anzahl", len(msgs), "typ", "unread")
|
||||
result, err := summarizeWithLLMModel(msgs, "Fasse diese ungelesenen Emails zusammen. Hebe wichtige, dringende oder actionable Emails hervor.", accountModel(acc))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Nach dem Zusammenfassen: Emails in Processed-Ordner verschieben
|
||||
if processedFolder != "" && len(seqNums) > 0 {
|
||||
if moveErr := cl.MoveMessages(seqNums, processedFolder); moveErr != nil {
|
||||
slog.Warn("Emails konnten nicht verschoben werden", "fehler", moveErr, "ordner", processedFolder)
|
||||
if acc.ProcessedFolder != "" && len(seqNums) > 0 {
|
||||
if moveErr := cl.MoveMessages(seqNums, acc.ProcessedFolder); moveErr != nil {
|
||||
slog.Warn("Emails konnten nicht verschoben werden", "fehler", moveErr, "ordner", acc.ProcessedFolder)
|
||||
} else {
|
||||
slog.Info("Emails verschoben", "anzahl", len(seqNums), "ordner", processedFolder)
|
||||
slog.Info("Emails verschoben", "anzahl", len(seqNums), "ordner", acc.ProcessedFolder)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// TriageRecentAllAccounts klassifiziert die letzten n Emails aller Accounts manuell
|
||||
// und verschiebt sie in die konfigurierten Triage-Ordner.
|
||||
func TriageRecentAllAccounts(n uint32) (string, error) {
|
||||
accounts := config.AllEmailAccounts()
|
||||
if len(accounts) == 0 {
|
||||
return "", fmt.Errorf("kein Email-Account konfiguriert")
|
||||
}
|
||||
|
||||
var lines []string
|
||||
for _, acc := range accounts {
|
||||
if acc.TriageImportantFolder == "" && acc.TriageUnimportantFolder == "" {
|
||||
lines = append(lines, fmt.Sprintf("⚠️ **%s:** kein triage_important_folder / triage_unimportant_folder konfiguriert", accountLabel(acc)))
|
||||
continue
|
||||
}
|
||||
wichtig, unwichtig, err := triageRecentAccount(acc, n)
|
||||
if err != nil {
|
||||
lines = append(lines, fmt.Sprintf("❌ **%s:** %v", accountLabel(acc), err))
|
||||
continue
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("✅ **%s:** %d wichtig → `%s`, %d unwichtig → `%s`",
|
||||
accountLabel(acc), wichtig, acc.TriageImportantFolder, unwichtig, acc.TriageUnimportantFolder))
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
// triageRecentAccount klassifiziert die letzten n Emails eines Accounts.
|
||||
// Gibt Anzahl wichtiger und unwichtiger Emails zurück.
|
||||
func triageRecentAccount(acc config.EmailAccount, n uint32) (wichtig, unwichtig int, err error) {
|
||||
cl, err := ConnectAccount(acc)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("verbinden: %w", err)
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
// Ordner vorab anlegen, unabhängig davon ob Emails verschoben werden
|
||||
if acc.TriageImportantFolder != "" {
|
||||
if err := cl.EnsureFolder(acc.TriageImportantFolder); err != nil {
|
||||
slog.Warn("[Triage] Ordner anlegen fehlgeschlagen", "ordner", acc.TriageImportantFolder, "fehler", err)
|
||||
}
|
||||
}
|
||||
if acc.TriageUnimportantFolder != "" {
|
||||
if err := cl.EnsureFolder(acc.TriageUnimportantFolder); err != nil {
|
||||
slog.Warn("[Triage] Ordner anlegen fehlgeschlagen", "ordner", acc.TriageUnimportantFolder, "fehler", err)
|
||||
}
|
||||
}
|
||||
|
||||
msgs, err := cl.FetchRecentForSelect(n)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("fetch: %w", err)
|
||||
}
|
||||
if len(msgs) == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
model := accountModel(acc)
|
||||
var wichtigSeqNums, unwichtigSeqNums []uint32
|
||||
|
||||
slog.Info("[Triage] Manuell gestartet", "account", accountLabel(acc), "anzahl", len(msgs))
|
||||
for _, msg := range msgs {
|
||||
if ClassifyImportance(msg.Message, model) {
|
||||
wichtigSeqNums = append(wichtigSeqNums, msg.SeqNum)
|
||||
} else {
|
||||
unwichtigSeqNums = append(unwichtigSeqNums, msg.SeqNum)
|
||||
}
|
||||
}
|
||||
|
||||
if acc.TriageUnimportantFolder != "" && len(unwichtigSeqNums) > 0 {
|
||||
if ensureErr := cl.EnsureFolder(acc.TriageUnimportantFolder); ensureErr != nil {
|
||||
slog.Warn("[Triage] Ordner anlegen fehlgeschlagen", "ordner", acc.TriageUnimportantFolder, "fehler", ensureErr)
|
||||
}
|
||||
if moveErr := cl.MoveMessages(unwichtigSeqNums, acc.TriageUnimportantFolder); moveErr != nil {
|
||||
slog.Warn("[Triage] Verschieben unwichtig fehlgeschlagen", "fehler", moveErr)
|
||||
}
|
||||
}
|
||||
|
||||
if acc.TriageImportantFolder != "" && len(wichtigSeqNums) > 0 {
|
||||
if ensureErr := cl.EnsureFolder(acc.TriageImportantFolder); ensureErr != nil {
|
||||
slog.Warn("[Triage] Ordner anlegen fehlgeschlagen", "ordner", acc.TriageImportantFolder, "fehler", ensureErr)
|
||||
}
|
||||
if moveErr := cl.MoveMessages(wichtigSeqNums, acc.TriageImportantFolder); moveErr != nil {
|
||||
slog.Warn("[Triage] Verschieben wichtig fehlgeschlagen", "fehler", moveErr)
|
||||
}
|
||||
}
|
||||
|
||||
return len(wichtigSeqNums), len(unwichtigSeqNums), nil
|
||||
}
|
||||
|
||||
// triageUnread klassifiziert alle ungelesenen Emails eines Accounts und verschiebt
|
||||
// wichtige in TriageImportantFolder und unwichtige in TriageUnimportantFolder.
|
||||
// Läuft sequentiell: eine Email nach der anderen.
|
||||
func triageUnread(acc config.EmailAccount) error {
|
||||
cl, err := ConnectAccount(acc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verbinden: %w", err)
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
// Ordner vorab anlegen
|
||||
if acc.TriageImportantFolder != "" {
|
||||
cl.EnsureFolder(acc.TriageImportantFolder)
|
||||
}
|
||||
if acc.TriageUnimportantFolder != "" {
|
||||
cl.EnsureFolder(acc.TriageUnimportantFolder)
|
||||
}
|
||||
|
||||
msgs, seqNums, err := cl.FetchUnreadSeqNums()
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch: %w", err)
|
||||
}
|
||||
if len(msgs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
model := accountModel(acc)
|
||||
var wichtigSeqNums, unwichtigSeqNums []uint32
|
||||
|
||||
slog.Info("[Triage] Klassifizierung gestartet", "account", accountLabel(acc), "anzahl", len(msgs))
|
||||
for i, msg := range msgs {
|
||||
if ClassifyImportance(msg, model) {
|
||||
wichtigSeqNums = append(wichtigSeqNums, seqNums[i])
|
||||
} else {
|
||||
unwichtigSeqNums = append(unwichtigSeqNums, seqNums[i])
|
||||
}
|
||||
}
|
||||
|
||||
if acc.TriageUnimportantFolder != "" && len(unwichtigSeqNums) > 0 {
|
||||
if err := cl.EnsureFolder(acc.TriageUnimportantFolder); err != nil {
|
||||
slog.Warn("[Triage] Ordner konnte nicht angelegt werden", "ordner", acc.TriageUnimportantFolder, "fehler", err)
|
||||
}
|
||||
if err := cl.MoveMessages(unwichtigSeqNums, acc.TriageUnimportantFolder); err != nil {
|
||||
slog.Warn("[Triage] Verschieben unwichtig fehlgeschlagen", "ordner", acc.TriageUnimportantFolder, "fehler", err)
|
||||
} else {
|
||||
slog.Info("[Triage] Unwichtige Emails verschoben", "anzahl", len(unwichtigSeqNums), "ordner", acc.TriageUnimportantFolder)
|
||||
}
|
||||
}
|
||||
|
||||
if acc.TriageImportantFolder != "" && len(wichtigSeqNums) > 0 {
|
||||
if err := cl.EnsureFolder(acc.TriageImportantFolder); err != nil {
|
||||
slog.Warn("[Triage] Ordner konnte nicht angelegt werden", "ordner", acc.TriageImportantFolder, "fehler", err)
|
||||
}
|
||||
if err := cl.MoveMessages(wichtigSeqNums, acc.TriageImportantFolder); err != nil {
|
||||
slog.Warn("[Triage] Verschieben wichtig fehlgeschlagen", "ordner", acc.TriageImportantFolder, "fehler", err)
|
||||
} else {
|
||||
slog.Info("[Triage] Wichtige Emails verschoben", "anzahl", len(wichtigSeqNums), "ordner", acc.TriageImportantFolder)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClassifyImportance klassifiziert eine einzelne Email als wichtig (true) oder unwichtig (false).
|
||||
// Sucht zuerst ähnliche vergangene Entscheidungen in Qdrant (RAG) und gibt sie als Kontext mit.
|
||||
// Im Fehlerfall oder bei unklarer Antwort wird true (wichtig) zurückgegeben – sicherer Default.
|
||||
func ClassifyImportance(msg Message, model string) bool {
|
||||
// RAG: ähnliche vergangene Triage-Entscheidungen als Few-Shot-Beispiele
|
||||
ragQuery := fmt.Sprintf("Von: %s Betreff: %s", msg.From, msg.Subject)
|
||||
examples := triage.SearchSimilar(ragQuery)
|
||||
|
||||
var examplesText string
|
||||
if len(examples) > 0 {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Ähnliche Entscheidungen aus der Vergangenheit:\n")
|
||||
for _, ex := range examples {
|
||||
sb.WriteString("- ")
|
||||
sb.WriteString(ex.Text)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
examplesText = sb.String()
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf("%sVon: %s\nBetreff: %s\n\nIst diese Email wichtig? Antworte NUR mit einem einzigen Wort: wichtig oder unwichtig.",
|
||||
examplesText, msg.From, msg.Subject)
|
||||
|
||||
chatClient := config.NewChatClient()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := chatClient.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
|
||||
Model: model,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{Role: openai.ChatMessageRoleSystem, Content: "Du bist ein Email-Filter. Antworte immer nur mit einem einzigen Wort: wichtig oder unwichtig."},
|
||||
{Role: openai.ChatMessageRoleUser, Content: prompt},
|
||||
},
|
||||
Temperature: 0.1,
|
||||
MaxTokens: 300,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("[Triage] LLM-Fehler, Email als wichtig eingestuft", "betreff", msg.Subject, "fehler", err)
|
||||
return true
|
||||
}
|
||||
if len(resp.Choices) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
raw := resp.Choices[0].Message.Content
|
||||
// Reasoning-Modelle (z.B. Qwen3) geben Antwort nach </think>-Tag aus
|
||||
if idx := strings.LastIndex(raw, "</think>"); idx >= 0 {
|
||||
raw = raw[idx+len("</think>"):]
|
||||
}
|
||||
answer := strings.ToLower(strings.TrimSpace(raw))
|
||||
isImportant := !strings.Contains(answer, "unwichtig")
|
||||
slog.Info("[Triage] Email klassifiziert",
|
||||
"betreff", msg.Subject,
|
||||
"von", msg.From,
|
||||
"wichtig", isImportant,
|
||||
"rag_beispiele", len(examples),
|
||||
"antwort", answer,
|
||||
)
|
||||
|
||||
// Entscheidung für künftiges Lernen in Qdrant speichern
|
||||
if err := triage.StoreDecision(msg.Subject, msg.From, isImportant); err != nil {
|
||||
slog.Warn("[Triage] Entscheidung nicht gespeichert", "fehler", err)
|
||||
}
|
||||
|
||||
return isImportant
|
||||
}
|
||||
|
||||
// ExtractReminders sucht in den letzten Emails nach Terminen/Deadlines.
|
||||
func ExtractReminders() (string, error) {
|
||||
return fetchAndSummarize(30, "Extrahiere alle Termine, Deadlines, Erinnerungen und wichtigen Daten aus diesen Emails. Liste sie strukturiert auf.")
|
||||
accounts := config.AllEmailAccounts()
|
||||
if len(accounts) == 0 {
|
||||
return "", fmt.Errorf("Kein Email-Account konfiguriert")
|
||||
}
|
||||
if len(accounts) == 1 {
|
||||
return fetchAndSummarizeAccount(accounts[0], 30, "Extrahiere alle Termine, Deadlines, Erinnerungen und wichtigen Daten aus diesen Emails. Liste sie strukturiert auf.")
|
||||
}
|
||||
var parts []string
|
||||
for _, acc := range accounts {
|
||||
result, err := fetchAndSummarizeAccount(acc, 30, "Extrahiere alle Termine, Deadlines, Erinnerungen und wichtigen Daten aus diesen Emails. Liste sie strukturiert auf.")
|
||||
if err != nil {
|
||||
slog.Error("Email-Fehler", "account", acc.Name, "fehler", err)
|
||||
parts = append(parts, fmt.Sprintf("❌ **%s:** %v", accountLabel(acc), err))
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("**%s:**\n%s", accountLabel(acc), result))
|
||||
}
|
||||
return strings.Join(parts, "\n\n"), nil
|
||||
}
|
||||
|
||||
// MoveUnread verschiebt alle ungelesenen Emails eines Accounts in den Zielordner.
|
||||
// Gibt die Anzahl verschobener Emails zurück.
|
||||
func MoveUnread(acc config.EmailAccount, destFolder string) (int, error) {
|
||||
cl, err := ConnectAccount(acc)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Email-Verbindung (%s): %w", accountLabel(acc), err)
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
_, seqNums, err := cl.FetchUnreadSeqNums()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Emails abrufen: %w", err)
|
||||
}
|
||||
if len(seqNums) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if err := cl.MoveMessages(seqNums, destFolder); err != nil {
|
||||
return 0, fmt.Errorf("Verschieben nach %s: %w", destFolder, err)
|
||||
}
|
||||
return len(seqNums), nil
|
||||
}
|
||||
|
||||
// AccountSelectMessages enthält ungelesene Emails eines Accounts für die Discord-Auswahl.
|
||||
type AccountSelectMessages struct {
|
||||
Account config.EmailAccount
|
||||
AccIndex int
|
||||
Messages []SelectMessage
|
||||
}
|
||||
|
||||
// FetchUnreadForSelectAllAccounts holt ungelesene Emails aller Accounts für die Discord-Auswahl.
|
||||
func FetchUnreadForSelectAllAccounts() ([]AccountSelectMessages, error) {
|
||||
accounts := config.AllEmailAccounts()
|
||||
var result []AccountSelectMessages
|
||||
for i, acc := range accounts {
|
||||
cl, err := ConnectAccount(acc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Verbindung %s: %w", accountLabel(acc), err)
|
||||
}
|
||||
msgs, err := cl.FetchUnreadForSelect()
|
||||
cl.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Emails %s: %w", accountLabel(acc), err)
|
||||
}
|
||||
result = append(result, AccountSelectMessages{
|
||||
Account: acc,
|
||||
AccIndex: i,
|
||||
Messages: msgs,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FetchRecentForSelectAllAccounts holt die letzten n Emails aller Accounts für die Discord-Auswahl.
|
||||
func FetchRecentForSelectAllAccounts(n uint32) ([]AccountSelectMessages, error) {
|
||||
accounts := config.AllEmailAccounts()
|
||||
var result []AccountSelectMessages
|
||||
for i, acc := range accounts {
|
||||
cl, err := ConnectAccount(acc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Verbindung %s: %w", accountLabel(acc), err)
|
||||
}
|
||||
msgs, err := cl.FetchRecentForSelect(n)
|
||||
cl.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Emails %s: %w", accountLabel(acc), err)
|
||||
}
|
||||
result = append(result, AccountSelectMessages{
|
||||
Account: acc,
|
||||
AccIndex: i,
|
||||
Messages: msgs,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MoveOldEmailsAllAccounts verschiebt alle Emails aller Accounts, die älter als olderThanDays Tage sind, nach destFolder.
|
||||
// Gibt die Gesamtanzahl verschobener Emails zurück.
|
||||
func MoveOldEmailsAllAccounts(destFolder string, olderThanDays int) (int, error) {
|
||||
accounts := config.AllEmailAccounts()
|
||||
total := 0
|
||||
for _, acc := range accounts {
|
||||
cl, err := ConnectAccount(acc)
|
||||
if err != nil {
|
||||
return total, fmt.Errorf("Verbindung %s: %w", accountLabel(acc), err)
|
||||
}
|
||||
n, err := cl.MoveOldMessages(acc.Folder, destFolder, olderThanDays)
|
||||
cl.Close()
|
||||
if err != nil {
|
||||
return total, fmt.Errorf("Verschieben %s: %w", accountLabel(acc), err)
|
||||
}
|
||||
total += n
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// MoveSpecificUnread verschiebt spezifische Emails (per Sequenznummer) eines Accounts in den Zielordner.
|
||||
func MoveSpecificUnread(accIndex int, seqNums []uint32, destFolder string) (int, error) {
|
||||
accounts := config.AllEmailAccounts()
|
||||
if accIndex < 0 || accIndex >= len(accounts) {
|
||||
return 0, fmt.Errorf("ungültiger Account-Index %d", accIndex)
|
||||
}
|
||||
acc := accounts[accIndex]
|
||||
cl, err := ConnectAccount(acc)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Email-Verbindung (%s): %w", accountLabel(acc), err)
|
||||
}
|
||||
defer cl.Close()
|
||||
if err := cl.MoveSpecificMessages(seqNums, destFolder); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(seqNums), nil
|
||||
}
|
||||
|
||||
// CleanupArchiveFolders löscht abgelaufene Emails aus allen konfigurierten Archivordnern.
|
||||
// Gibt eine menschenlesbare Zusammenfassung zurück.
|
||||
func CleanupArchiveFolders() (string, error) {
|
||||
accounts := config.AllEmailAccounts()
|
||||
var lines []string
|
||||
var errs []string
|
||||
total := 0
|
||||
|
||||
for _, acc := range accounts {
|
||||
for _, af := range acc.ArchiveFolders {
|
||||
if af.RetentionDays <= 0 {
|
||||
continue
|
||||
}
|
||||
cl, err := ConnectAccount(acc)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s/%s: %v", accountLabel(acc), af.Name, err))
|
||||
continue
|
||||
}
|
||||
n, err := cl.CleanupOldEmails(af.IMAPFolder, af.RetentionDays)
|
||||
cl.Close()
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s/%s: %v", accountLabel(acc), af.Name, err))
|
||||
continue
|
||||
}
|
||||
if n > 0 {
|
||||
lines = append(lines, fmt.Sprintf("🗑️ %s/%s: %d Email(s) gelöscht (älter als %d Tage)", accountLabel(acc), af.Name, n, af.RetentionDays))
|
||||
total += n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result string
|
||||
if len(lines) == 0 && len(errs) == 0 {
|
||||
result = "Kein Aufräumen notwendig."
|
||||
} else {
|
||||
result = strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
var combinedErr error
|
||||
if len(errs) > 0 {
|
||||
combinedErr = fmt.Errorf("%s", strings.Join(errs, "; "))
|
||||
}
|
||||
slog.Info("Archiv-Aufräumen abgeschlossen", "gelöscht", total, "fehler", len(errs))
|
||||
return result, combinedErr
|
||||
}
|
||||
|
||||
// SummarizeMessages fasst eine übergebene Liste von Nachrichten zusammen (für Tests ohne IMAP).
|
||||
@@ -72,8 +522,8 @@ func SummarizeMessages(msgs []Message, instruction string) (string, error) {
|
||||
return summarizeWithLLM(msgs, instruction)
|
||||
}
|
||||
|
||||
func fetchAndSummarize(n uint32, instruction string) (string, error) {
|
||||
cl, err := Connect()
|
||||
func fetchAndSummarizeAccount(acc config.EmailAccount, n uint32, instruction string) (string, error) {
|
||||
cl, err := ConnectAccount(acc)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Email-Verbindung: %w", err)
|
||||
}
|
||||
@@ -87,12 +537,27 @@ func fetchAndSummarize(n uint32, instruction string) (string, error) {
|
||||
return "📭 Keine Emails gefunden.", nil
|
||||
}
|
||||
|
||||
slog.Info("Email-Zusammenfassung gestartet", "anzahl", len(msgs))
|
||||
return summarizeWithLLM(msgs, instruction)
|
||||
slog.Info("Email-Zusammenfassung gestartet", "account", accountLabel(acc), "anzahl", len(msgs))
|
||||
return summarizeWithLLMModel(msgs, instruction, accountModel(acc))
|
||||
}
|
||||
|
||||
// emailModel gibt das konfigurierte Modell für Email-Zusammenfassungen zurück.
|
||||
// Fällt auf chat.model zurück wenn email.model nicht gesetzt ist.
|
||||
// accountLabel gibt einen lesbaren Namen für einen Account zurück.
|
||||
func accountLabel(acc config.EmailAccount) string {
|
||||
if acc.Name != "" {
|
||||
return acc.Name
|
||||
}
|
||||
return acc.User
|
||||
}
|
||||
|
||||
// accountModel gibt das konfigurierte LLM-Modell für einen Account zurück.
|
||||
func accountModel(acc config.EmailAccount) string {
|
||||
if acc.Model != "" {
|
||||
return acc.Model
|
||||
}
|
||||
return config.Cfg.Chat.Model
|
||||
}
|
||||
|
||||
// emailModel gibt das konfigurierte Modell für den Legacy-Account zurück.
|
||||
func emailModel() string {
|
||||
if config.Cfg.Email.Model != "" {
|
||||
return config.Cfg.Email.Model
|
||||
@@ -110,8 +575,11 @@ func formatEmailList(msgs []Message) string {
|
||||
}
|
||||
|
||||
func summarizeWithLLM(msgs []Message, instruction string) (string, error) {
|
||||
return summarizeWithLLMModel(msgs, instruction, emailModel())
|
||||
}
|
||||
|
||||
func summarizeWithLLMModel(msgs []Message, instruction, model string) (string, error) {
|
||||
emailList := formatEmailList(msgs)
|
||||
model := emailModel()
|
||||
|
||||
chatClient := config.NewChatClient()
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in New Issue
Block a user