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 }