d023df1b4a
Fixes from testing email search (docs/enhancements-2026-07-07.md): - New `folders` agent command lists the account's mailboxes (name, delimiter, selectable), INBOX first, so agents can discover archived mail outside INBOX. - `search --all-folders` sweeps every selectable mailbox; each hit carries a `folder` field, `skipped_folders` reports mailboxes the server refused, and --limit caps visible results across the sweep. The sweep deliberately skips EnsureFolderBaseline so a read-only search never mutates list --new state. - Empty search results include a generic `data.hint` with next steps. The hint is a fixed constant per mode, so the invisibility invariant holds: absent and policy-filtered mail produce byte-identical envelopes (codified in TestSearchEmptyHintIndistinguishableFromFiltered). - Skill and user docs: document `--text` full-text search as best-effort (server-dependent); recommend --subject-contains/--from. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
79 lines
1.7 KiB
Go
79 lines
1.7 KiB
Go
//go:build integration
|
|
|
|
package mail
|
|
|
|
import (
|
|
"os"
|
|
"strconv"
|
|
"testing"
|
|
)
|
|
|
|
// These tests require a GreenMail server reachable via env vars:
|
|
//
|
|
// EMCLI_TEST_IMAP_HOST, EMCLI_TEST_IMAP_PORT, EMCLI_TEST_USER, EMCLI_TEST_PASS
|
|
//
|
|
// Run: go test -tags=integration ./internal/mail/
|
|
func testCfg(t *testing.T) IMAPConfig {
|
|
t.Helper()
|
|
host := os.Getenv("EMCLI_TEST_IMAP_HOST")
|
|
if host == "" {
|
|
t.Skip("EMCLI_TEST_IMAP_HOST not set; skipping IMAP integration test")
|
|
}
|
|
return IMAPConfig{
|
|
Host: host,
|
|
Port: atoiEnv(t, "EMCLI_TEST_IMAP_PORT"),
|
|
Security: "starttls",
|
|
Username: os.Getenv("EMCLI_TEST_USER"),
|
|
Password: os.Getenv("EMCLI_TEST_PASS"),
|
|
}
|
|
}
|
|
|
|
func TestSelectAndFetch(t *testing.T) {
|
|
c, err := Dial(testCfg(t))
|
|
if err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
defer c.Logout()
|
|
|
|
uidv, maxUID, err := c.SelectFolder("INBOX")
|
|
if err != nil {
|
|
t.Fatalf("SelectFolder: %v", err)
|
|
}
|
|
if uidv == 0 {
|
|
t.Fatal("uidvalidity should be non-zero")
|
|
}
|
|
headers, err := c.FetchHeaders("INBOX", nil)
|
|
if err != nil {
|
|
t.Fatalf("FetchHeaders: %v", err)
|
|
}
|
|
t.Logf("inbox has %d messages, maxUID=%d", len(headers), maxUID)
|
|
}
|
|
|
|
func TestListFolders(t *testing.T) {
|
|
c, err := Dial(testCfg(t))
|
|
if err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
defer c.Logout()
|
|
|
|
folders, err := c.ListFolders()
|
|
if err != nil {
|
|
t.Fatalf("ListFolders: %v", err)
|
|
}
|
|
if len(folders) == 0 {
|
|
t.Fatal("expected at least one folder")
|
|
}
|
|
if folders[0].Name != "INBOX" || !folders[0].Selectable {
|
|
t.Fatalf("expected selectable INBOX first, got %+v", folders[0])
|
|
}
|
|
}
|
|
|
|
func atoiEnv(t *testing.T, k string) int {
|
|
t.Helper()
|
|
n, err := strconv.Atoi(os.Getenv(k))
|
|
if err != nil {
|
|
t.Fatalf("bad int env %s: %v", k, err)
|
|
}
|
|
return n
|
|
}
|