0969f76da6
imapclient.New gibt in beta.8 nur *Client zurueck (kein error). Alle drei Aufrufe in Connect() korrigiert. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
134 lines
3.4 KiB
Go
134 lines
3.4 KiB
Go
package imap
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
imapv2 "github.com/emersion/go-imap/v2"
|
|
"github.com/emersion/go-imap/v2/imapclient"
|
|
)
|
|
|
|
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.
|
|
var junkTrashNames = []string{
|
|
"junk", "spam", "trash", "deleted items",
|
|
"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.
|
|
// 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":
|
|
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)
|
|
}
|
|
c := imapclient.New(raw, nil)
|
|
return &Conn{Client: c, raw: raw}, nil
|
|
|
|
case "starttls":
|
|
raw, err := net.DialTimeout("tcp", addr, dialTimeout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("imap connect starttls: %w", err)
|
|
}
|
|
c := imapclient.New(raw, &imapclient.Options{
|
|
TLSConfig: &tls.Config{ServerName: host},
|
|
})
|
|
return &Conn{Client: c, raw: raw}, nil
|
|
|
|
case "none":
|
|
raw, err := net.DialTimeout("tcp", addr, dialTimeout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("imap connect plain: %w", err)
|
|
}
|
|
c := imapclient.New(raw, nil)
|
|
return &Conn{Client: c, raw: raw}, nil
|
|
|
|
default:
|
|
return nil, fmt.Errorf("imap: unknown tls mode %q", tlsMode)
|
|
}
|
|
}
|
|
|
|
// ListFolders retrieves all mailbox folders and detects junk/trash folders.
|
|
func ListFolders(c *imapclient.Client) ([]FolderInfo, error) {
|
|
listCmd := c.List("", "*", nil)
|
|
mailboxes, err := listCmd.Collect()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("imap list folders: %w", err)
|
|
}
|
|
|
|
var folders []FolderInfo
|
|
for _, mb := range mailboxes {
|
|
fi := FolderInfo{Name: mb.Mailbox}
|
|
|
|
// Check special-use attributes (RFC 6154)
|
|
for _, attr := range mb.Attrs {
|
|
if attr == imapv2.MailboxAttrJunk {
|
|
fi.Excluded = true
|
|
fi.Reason = "special_use"
|
|
break
|
|
}
|
|
if attr == imapv2.MailboxAttrTrash {
|
|
fi.Excluded = true
|
|
fi.Reason = "special_use"
|
|
break
|
|
}
|
|
}
|
|
|
|
// Fallback: case-insensitive name matching
|
|
if !fi.Excluded {
|
|
lower := strings.ToLower(mb.Mailbox)
|
|
for _, jt := range junkTrashNames {
|
|
if lower == jt {
|
|
fi.Excluded = true
|
|
fi.Reason = "name_match"
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
folders = append(folders, fi)
|
|
}
|
|
|
|
return folders, nil
|
|
}
|