zwischenstand
This commit is contained in:
153
internal/agents/tool/email/idle.go
Normal file
153
internal/agents/tool/email/idle.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// email/idle.go – IMAP IDLE Watcher für Echtzeit-Email-Benachrichtigungen
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
imap "github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapclient"
|
||||
|
||||
"my-brain-importer/internal/config"
|
||||
)
|
||||
|
||||
|
||||
// IdleWatcher überwacht einen IMAP-Account per IDLE auf neue Nachrichten.
|
||||
type IdleWatcher struct {
|
||||
acc config.EmailAccount
|
||||
onNew func(accountName, summary string)
|
||||
fetching atomic.Bool
|
||||
}
|
||||
|
||||
// NewIdleWatcher erstellt einen IdleWatcher für einen einzelnen Account.
|
||||
// onNew wird aufgerufen wenn neue Emails gefunden wurden (mit Account-Name und Zusammenfassung).
|
||||
func NewIdleWatcher(acc config.EmailAccount, onNew func(accountName, summary string)) *IdleWatcher {
|
||||
return &IdleWatcher{acc: acc, onNew: onNew}
|
||||
}
|
||||
|
||||
// Run startet die IDLE-Schleife. Blockiert bis ctx abgebrochen wird.
|
||||
func (w *IdleWatcher) Run(ctx context.Context) {
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
slog.Info("IDLE: Verbinde", "account", accountLabel(w.acc), "host", w.acc.Host)
|
||||
if err := w.runOnce(ctx); err != nil {
|
||||
slog.Warn("IDLE: Fehler, Neuverbindung in 60s", "account", accountLabel(w.acc), "fehler", err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(60 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *IdleWatcher) runOnce(ctx context.Context) error {
|
||||
// numMsgs wird atomar geschrieben/gelesen: UnilateralDataHandler läuft in einem
|
||||
// separaten Goroutine (imapclient-intern), IDLE-Loop liest im Hauptgoroutine.
|
||||
var numMsgs atomic.Uint32
|
||||
hasNew := make(chan struct{}, 1)
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", w.acc.Host, w.acc.Port)
|
||||
options := &imapclient.Options{
|
||||
UnilateralDataHandler: &imapclient.UnilateralDataHandler{
|
||||
Mailbox: func(data *imapclient.UnilateralDataMailbox) {
|
||||
if data.NumMessages != nil && *data.NumMessages > numMsgs.Load() {
|
||||
numMsgs.Store(*data.NumMessages)
|
||||
select {
|
||||
case hasNew <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
c *imapclient.Client
|
||||
err error
|
||||
)
|
||||
switch {
|
||||
case w.acc.TLS:
|
||||
tlsCfg := &tls.Config{ServerName: w.acc.Host}
|
||||
options.TLSConfig = tlsCfg
|
||||
c, err = imapclient.DialTLS(addr, options)
|
||||
case w.acc.StartTLS:
|
||||
tlsCfg := &tls.Config{ServerName: w.acc.Host}
|
||||
options.TLSConfig = tlsCfg
|
||||
c, err = imapclient.DialStartTLS(addr, options)
|
||||
default:
|
||||
c, err = imapclient.DialInsecure(addr, options)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("verbinden: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
c.Logout().Wait()
|
||||
c.Close()
|
||||
}()
|
||||
|
||||
if err := c.Login(w.acc.User, w.acc.Password).Wait(); err != nil {
|
||||
return fmt.Errorf("login: %w", err)
|
||||
}
|
||||
|
||||
folder := w.acc.Folder
|
||||
if folder == "" {
|
||||
folder = "INBOX"
|
||||
}
|
||||
selectData, err := c.Select(folder, &imap.SelectOptions{ReadOnly: true}).Wait()
|
||||
if err != nil {
|
||||
return fmt.Errorf("select: %w", err)
|
||||
}
|
||||
numMsgs.Store(selectData.NumMessages)
|
||||
slog.Info("IDLE: Aktiv", "account", accountLabel(w.acc), "folder", folder, "numMsgs", selectData.NumMessages)
|
||||
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
idleCmd, err := c.Idle()
|
||||
if err != nil {
|
||||
return fmt.Errorf("IDLE starten: %w", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
idleCmd.Close()
|
||||
idleCmd.Wait()
|
||||
return nil
|
||||
|
||||
case <-hasNew:
|
||||
idleCmd.Close()
|
||||
if err := idleCmd.Wait(); err != nil {
|
||||
slog.Warn("IDLE Wait Fehler", "account", accountLabel(w.acc), "fehler", err)
|
||||
}
|
||||
slog.Info("IDLE: Neue Email erkannt", "account", accountLabel(w.acc))
|
||||
// Nur einen gleichzeitigen Fetch erlauben
|
||||
if !w.fetching.Swap(true) {
|
||||
go func() {
|
||||
defer w.fetching.Store(false)
|
||||
w.notifyNewEmail()
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *IdleWatcher) notifyNewEmail() {
|
||||
summary, err := SummarizeUnreadAccount(w.acc)
|
||||
if err != nil {
|
||||
slog.Error("IDLE: Email-Zusammenfassung fehlgeschlagen", "account", accountLabel(w.acc), "fehler", err)
|
||||
return
|
||||
}
|
||||
if summary == "📭 Keine ungelesenen Emails." {
|
||||
return
|
||||
}
|
||||
w.onNew(accountLabel(w.acc), summary)
|
||||
}
|
||||
Reference in New Issue
Block a user