151 lines
3.5 KiB
Go
151 lines
3.5 KiB
Go
// email/client.go – IMAP-Client für Email-Abfragen
|
||
package email
|
||
|
||
import (
|
||
"crypto/tls"
|
||
"fmt"
|
||
|
||
imap "github.com/emersion/go-imap/v2"
|
||
"github.com/emersion/go-imap/v2/imapclient"
|
||
|
||
"my-brain-importer/internal/config"
|
||
)
|
||
|
||
// Message repräsentiert eine Email (ohne Body für schnelle Übersichten).
|
||
type Message struct {
|
||
Subject string
|
||
From string
|
||
Date string
|
||
}
|
||
|
||
// Client wraps die IMAP-Verbindung.
|
||
type Client struct {
|
||
c *imapclient.Client
|
||
}
|
||
|
||
// Connect öffnet eine IMAP-Verbindung.
|
||
func Connect() (*Client, error) {
|
||
cfg := config.Cfg.Email
|
||
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||
|
||
var (
|
||
c *imapclient.Client
|
||
err error
|
||
)
|
||
|
||
switch {
|
||
case cfg.TLS:
|
||
tlsCfg := &tls.Config{ServerName: cfg.Host}
|
||
c, err = imapclient.DialTLS(addr, &imapclient.Options{TLSConfig: tlsCfg})
|
||
case cfg.StartTLS:
|
||
tlsCfg := &tls.Config{ServerName: cfg.Host}
|
||
c, err = imapclient.DialStartTLS(addr, &imapclient.Options{TLSConfig: tlsCfg})
|
||
default:
|
||
c, err = imapclient.DialInsecure(addr, nil)
|
||
}
|
||
if err != nil {
|
||
return nil, fmt.Errorf("IMAP verbinden: %w", err)
|
||
}
|
||
|
||
if err := c.Login(cfg.User, cfg.Password).Wait(); err != nil {
|
||
c.Close()
|
||
return nil, fmt.Errorf("IMAP login: %w", err)
|
||
}
|
||
|
||
return &Client{c: c}, nil
|
||
}
|
||
|
||
// Close schließt die Verbindung.
|
||
func (cl *Client) Close() {
|
||
cl.c.Logout().Wait()
|
||
cl.c.Close()
|
||
}
|
||
|
||
// FetchRecent holt die letzten n Emails (Envelope-Daten, kein Body).
|
||
func (cl *Client) FetchRecent(n uint32) ([]Message, error) {
|
||
folder := config.Cfg.Email.Folder
|
||
if folder == "" {
|
||
folder = "INBOX"
|
||
}
|
||
|
||
selectData, err := cl.c.Select(folder, &imap.SelectOptions{ReadOnly: true}).Wait()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("IMAP select: %w", err)
|
||
}
|
||
if selectData.NumMessages == 0 {
|
||
return nil, nil
|
||
}
|
||
|
||
start := uint32(1)
|
||
if selectData.NumMessages > n {
|
||
start = selectData.NumMessages - n + 1
|
||
}
|
||
|
||
var seqSet imap.SeqSet
|
||
seqSet.AddRange(start, selectData.NumMessages)
|
||
|
||
msgs, err := cl.c.Fetch(seqSet, &imap.FetchOptions{Envelope: true}).Collect()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("IMAP fetch: %w", err)
|
||
}
|
||
|
||
return parseMessages(msgs), nil
|
||
}
|
||
|
||
// FetchUnread holt ungelesene Emails (Envelope-Daten, kein Body).
|
||
func (cl *Client) FetchUnread() ([]Message, error) {
|
||
folder := config.Cfg.Email.Folder
|
||
if folder == "" {
|
||
folder = "INBOX"
|
||
}
|
||
|
||
if _, err := cl.c.Select(folder, &imap.SelectOptions{ReadOnly: true}).Wait(); err != nil {
|
||
return nil, fmt.Errorf("IMAP select: %w", err)
|
||
}
|
||
|
||
searchData, err := cl.c.Search(&imap.SearchCriteria{
|
||
NotFlag: []imap.Flag{imap.FlagSeen},
|
||
}, nil).Wait()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("IMAP search: %w", err)
|
||
}
|
||
|
||
seqNums := searchData.AllSeqNums()
|
||
if len(seqNums) == 0 {
|
||
return nil, nil
|
||
}
|
||
|
||
var seqSet imap.SeqSet
|
||
seqSet.AddNum(seqNums...)
|
||
|
||
msgs, err := cl.c.Fetch(seqSet, &imap.FetchOptions{Envelope: true}).Collect()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("IMAP fetch: %w", err)
|
||
}
|
||
|
||
return parseMessages(msgs), nil
|
||
}
|
||
|
||
func parseMessages(msgs []*imapclient.FetchMessageBuffer) []Message {
|
||
result := make([]Message, 0, len(msgs))
|
||
for _, msg := range msgs {
|
||
if msg.Envelope == nil {
|
||
continue
|
||
}
|
||
m := Message{
|
||
Subject: msg.Envelope.Subject,
|
||
Date: msg.Envelope.Date.Format("2006-01-02 15:04"),
|
||
}
|
||
if len(msg.Envelope.From) > 0 {
|
||
addr := msg.Envelope.From[0]
|
||
if addr.Name != "" {
|
||
m.From = fmt.Sprintf("%s <%s@%s>", addr.Name, addr.Mailbox, addr.Host)
|
||
} else {
|
||
m.From = fmt.Sprintf("%s@%s", addr.Mailbox, addr.Host)
|
||
}
|
||
}
|
||
result = append(result, m)
|
||
}
|
||
return result
|
||
}
|