···11# Changelog
2233+## 2026-03-31
44+- **IMAP search across all folders (`space /` or `:search`)** — server-side IMAP SEARCH across all configured folders (Inbox, Sent, Archive, Feed, etc.); results displayed in a temporary "Search" tab with `[Folder]` prefix on each subject; supports query prefixes: `from:simon`, `subject:invoice`, `to:team@`, or plain text to search all three fields; press `esc` to close results
55+- **Filter preserves across actions** — the local `/` filter no longer clears when pressing `n` (toggle read), `m` (mark), `U` (clear marks), or sorting; filter stays active until `esc`
66+- **Address autocomplete in compose** — To, Cc, and Bcc fields show autocomplete suggestions from screener lists (`screened_in.txt`, `feed.txt`, `papertrail.txt`); navigate with `ctrl+n`/`ctrl+p`/arrows, accept with `tab`; supports multi-address fields (autocomplete applies after the last comma)
77+- **Everything view (`ge` or `:everything`)** — shows the 50 most recent emails across all folders in a temporary "Everything" tab, sorted by date descending; each subject prefixed with `[Folder]`; useful for finding emails that were screened out or moved to spam
88+- **Draft signature fix** — re-opening a draft (`E`) no longer appends a duplicate signature; the draft body already contains it from the first compose
99+- **Draft reader footer** — `E draft` now appears in the reader footer when viewing an email from the Drafts folder
1010+- **Android support (`make android`)** — cross-compile for Android ARM64; runs in Termux; documented in `docs/android.md` with install instructions and useful shortcuts
1111+- **Docs restructure** — detailed documentation moved from README to `docs/` folder: `docs/keybindings.md` (auto-generated), `docs/screener.md`, `docs/sending.md`, `docs/configuration.md`, `docs/android.md`; README kept concise with links
1212+313## 2026-03-30
414515- added preview email in $BROWSER (images rendered, same as recipient sees) with `p`
···2131- **Default screener paths** — changed from `~/.config/mutt/` to `~/.config/neomd/lists/` for new installs; existing configs with custom paths are unaffected
2232- **Go prerequisite check in Makefile** — `make build`/`make install` now prints clear Go installation instructions instead of a cryptic error when `go` is not found
2333- **Pre-send preview (`p`)** — press `p` in the pre-send screen to open a browser preview of the composed email; renders through the same goldmark pipeline as sending, with local image paths converted to `file://` URLs so inline images from `[attach]` lines display correctly
2424-- **Docs restructure** — detailed documentation moved from README to `docs/` folder: `docs/keybindings.md` (auto-generated), `docs/screener.md`, `docs/sending.md`, `docs/configuration.md`, `docs/android.md`; README kept concise with links
25342635## 2026-03-29
2736
+3
README.md
···7070- **Drafts** — `d` in pre-send saves to Drafts (IMAP APPEND); `E` in the reader re-opens a draft as an editable compose
7171- **Multiple From addresses** — define SMTP-only `[[senders]]` aliases (e.g. `s@ssp.sh` through an existing account); cycle with `ctrl+f` in compose and pre-send; sent copies always land in the Sent folder
7272- **Undo** — `u` reverses the last move or delete (`x`, `A`, `M*`) using the UIDPLUS destination UID
7373+- **Search** — `/` filters loaded emails in-memory; `space /` or `:search` runs IMAP SEARCH across all folders (only fetching header capped at 100 per folder) with results in a temporary "Search" tab; supports `from:`, `subject:`, `to:` prefixes
7474+- **Address autocomplete** — To/Cc/Bcc fields autocomplete from screener lists; navigate with `ctrl+n`/`ctrl+p`, accept with `tab`
7575+- **Everything view** — `ge` or `:everything` shows the 50 most recent emails across all folders; find emails that were screened out, moved to spam, or otherwise hard to locate
7376- **Glamour reading** — incoming emails rendered as styled Markdown in the terminal
7477- **HEY-style screener** — unknown senders land in `ToScreen`; press `I/O/F/P` to approve, block, mark as Feed, or mark as PaperTrail; reuses your existing `screened_in.txt` lists from neomutt
7578- **Folder tabs** — Inbox, ToScreen, Feed, PaperTrail, Archive, Waiting, Someday, Scheduled, Sent, Trash, ScreenedOut
+8-2
docs/keybindings.md
···3636| `gw` | go to Waiting |
3737| `gm` | go to Someday |
3838| `gd` | go to Drafts |
3939+| `ge` | go to Everything — latest 50 emails across all folders |
3940| `gS` | go to Spam (not in tab rotation) |
40414142···8283| Key | Action |
8384|-----|--------|
8485| `<space>1 … <space>9` | jump to folder tab by number (Inbox=1, ToScreen=2, …) |
8686+| `<space>/` | IMAP search ALL emails on server (From + Subject) |
858786888789### Sort (, prefix)
···136138| `:mark-read / :mr` | mark all emails in current folder as read |
137139| `:reload / :r` | reload current folder |
138140| `:check / :ch` | show screener classification for selected email |
141141+| `:everything / :ev` | show latest 50 emails across all folders |
142142+| `:search / :se` | IMAP search all emails on server (From + Subject + To) |
139143| `:delete-all / :da` | permanently delete ALL emails in current folder (y/n) |
140144| `:empty-trash / :et` | permanently delete ALL emails in Trash (y/n) |
141145| `:create-folders / :cf` | create missing IMAP folders from config (safe, idempotent) |
···147151148152| Key | Action |
149153|-----|--------|
150150-| `tab / enter` | move to next field |
154154+| `tab (To/Cc/Bcc)` | accept autocomplete suggestion or next field |
155155+| `ctrl+n / ctrl+p / arrows (To/Cc/Bcc)` | cycle through address suggestions |
151156| `enter (on Subject)` | open $EDITOR with a .md temp file |
152157| `esc` | cancel |
153158···156161157162| Key | Action |
158163|-----|--------|
159159-| `/` | filter emails in current folder |
164164+| `/` | filter loaded emails (From + Subject, in-memory) |
165165+| `<space>/ or :search` | IMAP search ALL emails on server (From + Subject) |
160166| `?` | toggle this help |
161167| `q` | quit (from inbox) |
162168
+150
internal/imap/client.go
···331331 return false
332332}
333333334334+// SearchMessages searches a folder using IMAP SEARCH and returns matching emails
335335+// with headers fetched. The query is matched against From, Subject, and To headers.
336336+// Supports prefixes: "from:x", "subject:x", "to:x". Plain text searches all three.
337337+// Searches ALL messages on the server, not just loaded ones.
338338+func (c *Client) SearchMessages(ctx context.Context, folder, query string) ([]Email, error) {
339339+ if ctx == nil {
340340+ ctx = context.Background()
341341+ }
342342+ if query == "" {
343343+ return nil, nil
344344+ }
345345+346346+ criteria := buildSearchCriteria(query)
347347+348348+ var uids []uint32
349349+ err := c.withConn(ctx, func(conn *imapclient.Client) error {
350350+ if err := c.selectMailbox(folder); err != nil {
351351+ return err
352352+ }
353353+354354+ searchData, err := conn.UIDSearch(criteria, nil).Wait()
355355+ if err != nil {
356356+ return fmt.Errorf("UID SEARCH: %w", err)
357357+ }
358358+ uidSet, ok := searchData.All.(imap.UIDSet)
359359+ if !ok {
360360+ return nil
361361+ }
362362+ nums, _ := uidSet.Nums()
363363+ for _, u := range nums {
364364+ uids = append(uids, uint32(u))
365365+ }
366366+ return nil
367367+ })
368368+ if err != nil {
369369+ return nil, err
370370+ }
371371+ if len(uids) == 0 {
372372+ return nil, nil
373373+ }
374374+375375+ // Cap results per folder to avoid huge fetches
376376+ if len(uids) > 100 {
377377+ uids = uids[len(uids)-100:] // keep newest (highest UIDs)
378378+ }
379379+380380+ return c.FetchHeadersByUID(ctx, folder, uids)
381381+}
382382+383383+// SearchAllFolders searches across multiple folders and returns combined results.
384384+// Each folder is SELECTed and searched individually (IMAP limitation).
385385+func (c *Client) SearchAllFolders(ctx context.Context, folders []string, query string) ([]Email, error) {
386386+ if query == "" {
387387+ return nil, nil
388388+ }
389389+ var all []Email
390390+ for _, folder := range folders {
391391+ emails, err := c.SearchMessages(ctx, folder, query)
392392+ if err != nil {
393393+ // Skip folders that fail (e.g. don't exist on this server)
394394+ continue
395395+ }
396396+ all = append(all, emails...)
397397+ }
398398+ return all, nil
399399+}
400400+401401+// buildSearchCriteria parses a query string into IMAP SearchCriteria.
402402+// Supports prefixes: "from:value", "subject:value", "to:value".
403403+// Plain text without a prefix searches OR(FROM, SUBJECT, TO).
404404+func buildSearchCriteria(query string) *imap.SearchCriteria {
405405+ q := strings.TrimSpace(query)
406406+ lower := strings.ToLower(q)
407407+408408+ switch {
409409+ case strings.HasPrefix(lower, "from:"):
410410+ val := strings.TrimSpace(q[5:])
411411+ return &imap.SearchCriteria{
412412+ Header: []imap.SearchCriteriaHeaderField{{Key: "From", Value: val}},
413413+ }
414414+ case strings.HasPrefix(lower, "subject:"):
415415+ val := strings.TrimSpace(q[8:])
416416+ return &imap.SearchCriteria{
417417+ Header: []imap.SearchCriteriaHeaderField{{Key: "Subject", Value: val}},
418418+ }
419419+ case strings.HasPrefix(lower, "to:"):
420420+ val := strings.TrimSpace(q[3:])
421421+ return &imap.SearchCriteria{
422422+ Header: []imap.SearchCriteriaHeaderField{{Key: "To", Value: val}},
423423+ }
424424+ default:
425425+ // Plain text: OR(FROM q, OR(SUBJECT q, TO q))
426426+ return &imap.SearchCriteria{
427427+ Or: [][2]imap.SearchCriteria{
428428+ {
429429+ {Header: []imap.SearchCriteriaHeaderField{{Key: "From", Value: q}}},
430430+ {Or: [][2]imap.SearchCriteria{
431431+ {
432432+ {Header: []imap.SearchCriteriaHeaderField{{Key: "Subject", Value: q}}},
433433+ {Header: []imap.SearchCriteriaHeaderField{{Key: "To", Value: q}}},
434434+ },
435435+ }},
436436+ },
437437+ },
438438+ }
439439+ }
440440+}
441441+442442+// FetchLatest fetches the N most recent emails (by UID, descending) from a folder.
443443+// Uses UID SEARCH ALL to get all UIDs, takes the last N, and fetches headers.
444444+func (c *Client) FetchLatest(ctx context.Context, folder string, n int) ([]Email, error) {
445445+ uids, err := c.SearchUIDs(ctx, folder)
446446+ if err != nil {
447447+ return nil, err
448448+ }
449449+ if len(uids) == 0 {
450450+ return nil, nil
451451+ }
452452+ // UIDs are ascending; take the last N (newest)
453453+ if len(uids) > n {
454454+ uids = uids[len(uids)-n:]
455455+ }
456456+ return c.FetchHeadersByUID(ctx, folder, uids)
457457+}
458458+459459+// FetchLatestAllFolders fetches the N most recent emails across all given folders,
460460+// sorted by date descending. Takes a few per folder proportionally.
461461+func (c *Client) FetchLatestAllFolders(ctx context.Context, folders []string, total int) ([]Email, error) {
462462+ perFolder := total / len(folders)
463463+ if perFolder < 5 {
464464+ perFolder = 5
465465+ }
466466+ var all []Email
467467+ for _, folder := range folders {
468468+ emails, err := c.FetchLatest(ctx, folder, perFolder)
469469+ if err != nil {
470470+ continue // skip folders that fail
471471+ }
472472+ all = append(all, emails...)
473473+ }
474474+ // Sort by date descending and cap
475475+ sort.Slice(all, func(i, j int) bool {
476476+ return all[i].Date.After(all[j].Date)
477477+ })
478478+ if len(all) > total {
479479+ all = all[:total]
480480+ }
481481+ return all, nil
482482+}
483483+334484// FetchHeadersByUID fetches envelope headers for a specific slice of UIDs.
335485// Callers should pass small batches (≤200) to avoid oversized IMAP requests.
336486func (c *Client) FetchHeadersByUID(ctx context.Context, folder string, uids []uint32) ([]Email, error) {
+17
internal/screener/screener.go
···8484 return s, nil
8585}
86868787+// AllAddresses returns a deduplicated slice of all known email addresses
8888+// from screened_in, feed, and papertrail lists. Useful for autocomplete.
8989+// Excludes screened_out and spam since you wouldn't want to email those.
9090+func (s *Screener) AllAddresses() []string {
9191+ seen := make(map[string]bool)
9292+ var addrs []string
9393+ for _, m := range []map[string]bool{s.screenedIn, s.feed, s.paperTrail} {
9494+ for addr := range m {
9595+ if !seen[addr] {
9696+ seen[addr] = true
9797+ addrs = append(addrs, addr)
9898+ }
9999+ }
100100+ }
101101+ return addrs
102102+}
103103+87104// Classify returns the category for a given "from" email address.
88105// The address is normalised to lowercase before matching.
89106func (s *Screener) Classify(from string) Category {