Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

# 2026-10-06

- **The Drafts tab showed the Inbox** — "Drafts" is in `defaultTabOrder`, so it
renders as a regular tab, but `activeFolder()`'s label→mailbox switch had no
`"Drafts"` case: every unmatched label fell through to `default` and fetched
the Inbox mailbox, so the tab displayed Inbox mail no matter what the webmail
showed in the real Drafts folder. Only the tab was affected — `gd` (off-tab
Drafts view) maps through a separate switch and was always correct. Added the
missing case (`internal/ui/model.go`, `activeFolder`). Side effect: `S` /
`:screen` now correctly refuse on the Drafts tab (`onPlainInboxTab`), where
they previously misidentified it as the Inbox. Test:
`TestActiveFolderDraftsTab`.

# 2026-10-05

- **The folder window is the newest mail by arrival date, not the highest UIDs (#34)** — `FetchHeaders` picked its `inbox_count` window as the highest `n` UIDs. IMAP MOVE/COPY give a message a fresh, highest UID in the destination mailbox (RFC 9051 per-mailbox UIDs), so every mail bulk-moved *into* a folder — `I` approve on ToScreen, `:reset-toscreen`, `U` undo — landed above all real recent mail, filled the window, and pushed the recent mail below the cutoff where no display sort (`,m`) could bring it back. Only folders with more than `inbox_count` messages were affected, which is why a HEY-style Inbox of a dozen rows never showed it while a 12'000-mail legacy Inbox after its first screening did (reproduced on Hostpoint/Dovecot, so not an Infomaniak quirk; the demo's `moves.log` shows `ToScreen uid=273 → INBOX destUID=1397`). `FetchHeaders`, `FetchLatest` (`:everything`) and the per-folder search cap (`searchFolderCap`, was a literal 100) now narrow the window with one cheap `UID FETCH (UID INTERNALDATE)` and keep the `n` most recently received (`newestUIDsByInternalDate`, `internal/imap/client.go`); INTERNALDATE survives MOVE/COPY on every server tested, and the SORT extension is not used because Infomaniak does not offer it. Folders at or below `inbox_count`, and `inbox_count = 0`, take the old single-round-trip path unchanged, so a daily Inbox sees no new traffic. Verified on the live Hostpoint demo: a 10-row window returned April mail before, October mail after. Tests: `TestMem_FetchHeaders_WindowIsNewestByInternalDateNotUID`, `TestMem_FetchHeaders_FolderWithinLimitUnchanged`, `TestMem_FetchLatest_WindowIsNewestByInternalDateNotUID`, `TestMem_SearchFolder_CapIsNewestByInternalDateNotUID`.
Expand Down
2 changes: 2 additions & 0 deletions internal/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,8 @@ func (m Model) activeFolder() string {
return m.cfg.Folders.Sent
case "Trash":
return m.cfg.Folders.Trash
case "Drafts":
return m.cfg.Folders.Drafts
case "Archive":
return m.cfg.Folders.Archive
case "Waiting":
Expand Down
19 changes: 19 additions & 0 deletions internal/ui/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,25 @@ func TestActiveFolderUsesOffTabFolder(t *testing.T) {
}
}

// TestActiveFolderDraftsTab — the Drafts tab (in defaultTabOrder) must fetch
// the Drafts mailbox; the label switch in activeFolder() had no Drafts case,
// so the tab fell through to default and showed the Inbox instead.
func TestActiveFolderDraftsTab(t *testing.T) {
m := Model{
cfg: &config.Config{
Folders: config.FoldersConfig{
Inbox: "INBOX",
Drafts: "Drafts",
},
},
folders: []string{"Inbox", "Drafts"},
activeFolderI: 1,
}
if got := m.activeFolder(); got != "Drafts" {
t.Fatalf("activeFolder() on the Drafts tab = %q, want %q (must not fall back to the Inbox)", got, "Drafts")
}
}

func TestUpdateInboxEscClearsCommittedFilter(t *testing.T) {
m := Model{
filterText: "invoice",
Expand Down