Files
claw-email/docs/superpowers/plans/2026-04-27-imap-proxy-support.md
T
thuanleandClaude Opus 4.7 c55a27d710
CI / fmt (pull_request) Failing after 4m52s
CI / test (pull_request) Successful in 10m50s
feat(mail): wire resolveThreadID into processMessage
- Add references variable to extract References header
- Replace manual inReplyTo handling with resolveThreadID call
- Maintain existing body parsing functionality
- Includes proper lambda function for database lookups

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 05:07:40 +07:00

6.4 KiB

IMAP Proxy Support Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make IMAP connections route through IMAP_PROXY_URL when configured, while keeping current direct dial behavior when unset.

Architecture: Introduce a small dial abstraction in IMAP watcher so connection setup is testable and can branch between direct TLS dial and SOCKS5 proxy dial. Keep authentication/fetch logic unchanged; only connection creation path changes. Validate proxy URL format early and fail clearly when invalid.

Tech Stack: Go, github.com/emersion/go-imap/v2/imapclient, golang.org/x/net/proxy, crypto/tls.


Task 1: Add failing tests for dial-path behavior

Files:

  • Modify: internal/mail/imap_test.go (create)

  • Modify: internal/mail/imap.go (for injectable dial function used by tests)

  • Step 1: Write failing test for direct dial when proxy unset

func TestConnectAndWatch_UsesDirectDial_WhenProxyUnset(t *testing.T) {
    watcher := &IMAPWatcher{cfg: &config.Config{IMAPHost: "imap.example.com", IMAPPort: "993", IMAPUser: "u", IMAPPass: "p"}}

    var directCalled bool
    watcher.dialIMAP = func(addr string, _ *imapclient.Options) (*imapclient.Client, error) {
        directCalled = true
        return nil, errors.New("stop")
    }

    _ = watcher.connectAndWatch(context.Background())

    if !directCalled {
        t.Fatal("expected direct dial path")
    }
}
  • Step 2: Write failing test for proxy dial when proxy set
func TestConnectAndWatch_UsesProxyDial_WhenProxySet(t *testing.T) {
    watcher := &IMAPWatcher{cfg: &config.Config{IMAPHost: "imap.example.com", IMAPPort: "993", IMAPUser: "u", IMAPPass: "p", IMAPProxyURL: "socks5://127.0.0.1:1080"}}

    var proxyCalled bool
    watcher.dialIMAPViaProxy = func(addr, proxyURL string, _ *imapclient.Options) (*imapclient.Client, error) {
        proxyCalled = true
        return nil, errors.New("stop")
    }

    _ = watcher.connectAndWatch(context.Background())

    if !proxyCalled {
        t.Fatal("expected proxy dial path")
    }
}
  • Step 3: Run tests to verify RED

Run: go test ./internal/mail -run Uses -v Expected: FAIL because dialIMAP / dialIMAPViaProxy hooks do not exist yet.

  • Step 4: Commit test scaffold after it compiles with new hooks (still failing behavior checks if needed)
git add internal/mail/imap_test.go internal/mail/imap.go
git commit -m "test(mail): add dial path tests for IMAP proxy"

Task 2: Implement proxy-aware dialing with minimal changes

Files:

  • Modify: internal/mail/imap.go

  • Step 1: Add dial hooks to watcher for testability

type IMAPWatcher struct {
    cfg    *config.Config
    db     *gorm.DB
    client *imapclient.Client

    dialIMAP         func(addr string, options *imapclient.Options) (*imapclient.Client, error)
    dialIMAPViaProxy func(addr, proxyURL string, options *imapclient.Options) (*imapclient.Client, error)

    OnReceived func(task *database.Task)
}
  • Step 2: Initialize default dial hooks in constructor
func NewIMAPWatcher(cfg *config.Config, db *gorm.DB) *IMAPWatcher {
    return &IMAPWatcher{
        cfg: cfg,
        db:  db,
        dialIMAP: imapclient.DialTLS,
        dialIMAPViaProxy: dialTLSViaSOCKS5,
    }
}
  • Step 3: Branch connection setup by IMAPProxyURL
if strings.TrimSpace(w.cfg.IMAPProxyURL) == "" {
    c, err = w.dialIMAP(addr, options)
} else {
    c, err = w.dialIMAPViaProxy(addr, w.cfg.IMAPProxyURL, options)
}
if err != nil {
    return fmt.Errorf("dial IMAP: %w", err)
}
  • Step 4: Implement SOCKS5 TLS dial helper
func dialTLSViaSOCKS5(addr, proxyURL string, options *imapclient.Options) (*imapclient.Client, error) {
    u, err := url.Parse(proxyURL)
    if err != nil {
        return nil, fmt.Errorf("parse proxy url: %w", err)
    }
    if u.Scheme != "socks5" {
        return nil, fmt.Errorf("unsupported proxy scheme: %s", u.Scheme)
    }

    var auth *proxy.Auth
    if u.User != nil {
        pw, _ := u.User.Password()
        auth = &proxy.Auth{User: u.User.Username(), Password: pw}
    }

    d, err := proxy.SOCKS5("tcp", u.Host, auth, proxy.Direct)
    if err != nil {
        return nil, fmt.Errorf("create socks5 dialer: %w", err)
    }

    conn, err := d.Dial("tcp", addr)
    if err != nil {
        return nil, fmt.Errorf("proxy dial: %w", err)
    }

    host, _, _ := net.SplitHostPort(addr)
    tlsConn := tls.Client(conn, &tls.Config{ServerName: host})
    if err := tlsConn.Handshake(); err != nil {
        _ = tlsConn.Close()
        return nil, fmt.Errorf("tls handshake: %w", err)
    }

    return imapclient.New(tlsConn, options), nil
}
  • Step 5: Run focused tests to verify GREEN

Run: go test ./internal/mail -run Uses -v Expected: PASS.

  • Step 6: Commit implementation
git add internal/mail/imap.go internal/mail/imap_test.go
git commit -m "feat(mail): support IMAP SOCKS5 proxy dialing"

Task 3: Add URL validation coverage and docs consistency

Files:

  • Modify: internal/mail/imap_test.go

  • Modify: README.md (only if wording needs tighter scheme requirement)

  • Step 1: Write test for invalid proxy scheme

func TestDialTLSViaSOCKS5_RejectsNonSocks5Scheme(t *testing.T) {
    _, err := dialTLSViaSOCKS5("imap.example.com:993", "http://proxy:8080", nil)
    if err == nil || !strings.Contains(err.Error(), "unsupported proxy scheme") {
        t.Fatalf("expected unsupported scheme error, got %v", err)
    }
}
  • Step 2: Run targeted test

Run: go test ./internal/mail -run RejectsNonSocks5Scheme -v Expected: PASS.

  • Step 3: Ensure README explicitly says socks5 scheme (already present, keep if unchanged)
IMAP_PROXY_URL: socks5://user:pass@host:port
  • Step 4: Commit doc/test alignment
git add internal/mail/imap_test.go README.md
git commit -m "test/docs: validate IMAP proxy URL scheme"

Task 4: Final verification

Files:

  • No new files expected

  • Step 1: Run package tests for mail

Run: go test ./internal/mail -v Expected: PASS.

  • Step 2: Run full suite

Run: go test ./... Expected: PASS.

  • Step 3: Push and update issue/PR with evidence
git push -u origin <branch>

Include commands and pass results in issue/PR comment.