feat(search): server-side email search#1186
Conversation
Adds an EmailSearcher interface to backend.Provider: - backend.SearchQuery + ParseSearchQuery DSL (from:, to:, subject:, body:, since:, before:, larger:) - IMAP backend: ESEARCH RETURN ALL when supported, falls back to UID SEARCH - JMAP backend: Email/query with FilterCondition - POP3 backend: returns ErrNotSupported - TUI overlay triggered by '/' from inbox; results applied as a temporary view, Esc returns to normal inbox - Default keybind 'inbox.search' = '/' - Tests for parser, JMAP filter assembly, fetcher criteria mapping, POP3 unsupported Closes floatpane#508. Partial: floatpane#1131.
- ParseSearchQuery: quote-aware tokenizer; preserve bare body terms when other fields are set; body: prefix wins over bare terms - tui/search: do not set done=true on backend error; results pane no longer shows 'Press Enter to apply' when search failed - main: partial results across accounts; skip ErrNotSupported instead of aborting; suppress parent ESC navigation when inbox search overlay or active state was just consumed - tui/inbox: expose IsSearchActive() for parent state introspection - backend tests: cover quoted values, mixed bare+prefixed terms, body precedence
floatpanebot
left a comment
There was a problem hiding this comment.
Hi @mvanhorn! Please fix the following issues with your PR:
- Title: Is too long (62 characters). The PR title must be strictly under 40 characters.
Formatting issues have been resolved. Thank you!
|
Okay, this is a great issue to implement! There are a few problems, right off the bat, not looking at code:
Please, fix these things first, and I'll get around checking this PR and refining if needed later (may take several days, quite busy atm) |
Per @andrinoff's review: 1. Account label in search/All-Accounts views now resolves to the To: recipient that matches the account's FetchEmail (parsed via net/mail), so multi-account setups where one upstream mailbox serves multiple FetchEmails can see which account each result was actually addressed to. Falls back to the account's FetchEmail when no recipient matches. Also fixes SetEmails which was tabbing accounts by acc.Email instead of FetchEmail. 2. Switching account tabs while search is active now filters searchResults in memory (filteredSearchResults), so the cross- account search executed against "All Accounts" can be narrowed to a single account without re-running the query. 3. Restored the client-side bubble-list filter under the new inbox.filter keybind (default 'f'). '/' continues to launch the server-side search overlay. Both surface in the keymap; help text shows 'f filter' alongside '/ search'. Plus a gofmt fix on screenshots/cmd/search_view/main.go that the lint job flagged. Tests: TestInboxSearchResultsFilterByActiveAccountTab, TestInboxAccountLabelUsesMatchingRecipient, TestInboxClientSideFilterKeyStartsListFilter, TestInboxMultiAccountTabs - all pass on macOS. Full TUI suite passes (go test ./tui/...).
|
Pushed cff22ef addressing all three points:
Verified: go test ./tui/... passes locally. Also fixed the lint failure on screenshots/cmd/search_view/main.go. Let me know if you want any of these tuned differently, particularly the keybind choice for filter. |
Followup on @andrinoff's review of floatpane#1186: 1. All-Accounts view and search results now dedupe by Message-ID (with a From|Subject|Date.UnixNano() fallback for malformed messages). When one upstream mailbox serves multiple FetchEmails, the same message no longer shows up once per account; the surviving row prefers the copy whose owning account's FetchEmail matches a To: recipient, and accountLabelForEmail now resolves across the full accounts list. Per-account tab views are unchanged. 5. ViewEmailMsg now carries an optional *fetcher.Email. The Inbox embeds it when opening a row from search results, so opening a search hit that wasn't in the local cache no longer silently drops the keypress: main.go prefers msg.Email, adds the email to m.emailsByAcct/m.emails when missing (so subsequent navigation works), and falls through to the existing FetchEmailBody path. Tests: TestInboxAllAccountsDedupesSharedMailboxByMessageID, TestInboxSearchResultsDedupedAcrossAccounts, TestInboxAllAccountsDoesNotDedupeWhenMessageIDDiffers, TestInboxAccountLabelUsesMatchingRecipient (updated for cross- account match), TestInboxOpenSearchResultEmbedsEmailInViewMsg. go test ./... is green locally.
|
Pushed 870aa1d addressing the two remaining points.
Verified locally: |
interesting, haven't looked at the diff, but the issue seems to persist with split view mode (#1006), although, it does work in full screen mode, maybe you can find the culprit |
|
Pushed 8fc9e1d. Root cause was a parallel split-pane code path: OpenSplitPreview discarded ViewEmailMsg.Email and PreviewBodyFetchedMsg.findEmailByUID only searched m.inbox.allEmails, which never contains search hits. Threaded the resolved email through OpenSplitPreview onto FolderInbox.previewSearchEmail and made findEmailByUID fall back to it when allEmails has no match. Verified locally: go test ./... is green, including two new tests in tui/folder_inbox_test.go that cover the cross-folder search-hit case and confirm the live allEmails entry still wins when both are present. |
Per @andrinoff's review: 1. Account label in search/All-Accounts views now resolves to the To: recipient that matches the account's FetchEmail (parsed via net/mail), so multi-account setups where one upstream mailbox serves multiple FetchEmails can see which account each result was actually addressed to. Falls back to the account's FetchEmail when no recipient matches. Also fixes SetEmails which was tabbing accounts by acc.Email instead of FetchEmail. 2. Switching account tabs while search is active now filters searchResults in memory (filteredSearchResults), so the cross- account search executed against "All Accounts" can be narrowed to a single account without re-running the query. 3. Restored the client-side bubble-list filter under the new inbox.filter keybind (default 'f'). '/' continues to launch the server-side search overlay. Both surface in the keymap; help text shows 'f filter' alongside '/ search'. Plus a gofmt fix on screenshots/cmd/search_view/main.go that the lint job flagged. Tests: TestInboxSearchResultsFilterByActiveAccountTab, TestInboxAccountLabelUsesMatchingRecipient, TestInboxClientSideFilterKeyStartsListFilter, TestInboxMultiAccountTabs - all pass on macOS. Full TUI suite passes (go test ./tui/...).
Followup on @andrinoff's review of floatpane#1186: 1. All-Accounts view and search results now dedupe by Message-ID (with a From|Subject|Date.UnixNano() fallback for malformed messages). When one upstream mailbox serves multiple FetchEmails, the same message no longer shows up once per account; the surviving row prefers the copy whose owning account's FetchEmail matches a To: recipient, and accountLabelForEmail now resolves across the full accounts list. Per-account tab views are unchanged. 5. ViewEmailMsg now carries an optional *fetcher.Email. The Inbox embeds it when opening a row from search results, so opening a search hit that wasn't in the local cache no longer silently drops the keypress: main.go prefers msg.Email, adds the email to m.emailsByAcct/m.emails when missing (so subsequent navigation works), and falls through to the existing FetchEmailBody path. Tests: TestInboxAllAccountsDedupesSharedMailboxByMessageID, TestInboxSearchResultsDedupedAcrossAccounts, TestInboxAllAccountsDoesNotDedupeWhenMessageIDDiffers, TestInboxAccountLabelUsesMatchingRecipient (updated for cross- account match), TestInboxOpenSearchResultEmbedsEmailInViewMsg. go test ./... is green locally.
8fc9e1d to
5cfd64c
Compare
Per @andrinoff's review: 1. Account label in search/All-Accounts views now resolves to the To: recipient that matches the account's FetchEmail (parsed via net/mail), so multi-account setups where one upstream mailbox serves multiple FetchEmails can see which account each result was actually addressed to. Falls back to the account's FetchEmail when no recipient matches. Also fixes SetEmails which was tabbing accounts by acc.Email instead of FetchEmail. 2. Switching account tabs while search is active now filters searchResults in memory (filteredSearchResults), so the cross- account search executed against "All Accounts" can be narrowed to a single account without re-running the query. 3. Restored the client-side bubble-list filter under the new inbox.filter keybind (default 'f'). '/' continues to launch the server-side search overlay. Both surface in the keymap; help text shows 'f filter' alongside '/ search'. Plus a gofmt fix on screenshots/cmd/search_view/main.go that the lint job flagged. Tests: TestInboxSearchResultsFilterByActiveAccountTab, TestInboxAccountLabelUsesMatchingRecipient, TestInboxClientSideFilterKeyStartsListFilter, TestInboxMultiAccountTabs - all pass on macOS. Full TUI suite passes (go test ./tui/...).
Followup on @andrinoff's review of floatpane#1186: 1. All-Accounts view and search results now dedupe by Message-ID (with a From|Subject|Date.UnixNano() fallback for malformed messages). When one upstream mailbox serves multiple FetchEmails, the same message no longer shows up once per account; the surviving row prefers the copy whose owning account's FetchEmail matches a To: recipient, and accountLabelForEmail now resolves across the full accounts list. Per-account tab views are unchanged. 5. ViewEmailMsg now carries an optional *fetcher.Email. The Inbox embeds it when opening a row from search results, so opening a search hit that wasn't in the local cache no longer silently drops the keypress: main.go prefers msg.Email, adds the email to m.emailsByAcct/m.emails when missing (so subsequent navigation works), and falls through to the existing FetchEmailBody path. Tests: TestInboxAllAccountsDedupesSharedMailboxByMessageID, TestInboxSearchResultsDedupedAcrossAccounts, TestInboxAllAccountsDoesNotDedupeWhenMessageIDDiffers, TestInboxAccountLabelUsesMatchingRecipient (updated for cross- account match), TestInboxOpenSearchResultEmbedsEmailInViewMsg. go test ./... is green locally.
OpenSplitPreview now accepts the resolved *fetcher.Email and stores it on FolderInbox; findEmailByUID falls back to that snapshot when the UID is not in m.inbox.allEmails (cross-folder or uncached search hits). Without this, the keypress to open a search result in split-pane mode was silently dropped because PreviewBodyFetchedMsg looked up the email via findEmailByUID(allEmails) and got nil. Tests: TestFolderInboxSplitPreviewRendersSearchHit covers the search-hit fallback; TestFolderInboxSplitPreviewPrefersAllEmails covers that the live allEmails entry still wins when present.
5cfd64c to
3400f03
Compare
Signed-off-by: drew <me@andrinoff.com>
Signed-off-by: drew <me@andrinoff.com>
Signed-off-by: drew <me@andrinoff.com>
Signed-off-by: drew <me@andrinoff.com>
Signed-off-by: drew <me@andrinoff.com>
|
/approve |
floatpanebot
left a comment
There was a problem hiding this comment.
Approved on behalf of @andrinoff via /approve command.

What?
EmailSearcherinterface (Search(ctx, folder, SearchQuery) ([]Email, error)) tobackend.ProviderSearchQuerystruct +ParseSearchQueryDSL parser supportingfrom:,to:,subject:,body:,since:,before:,larger:, plus quoted multi-word values and bare body termsfetcher.SearchMailboxusesgo-imap/v2UIDSearch, prefersESEARCH RETURN (ALL)(RFC 4731) whenimap.CapESearchorimap.CapIMAP4rev2is advertised, falls back to UIDSEARCHfor older serversEmail/querywithFilterCondition(From,To,Subject,Body,After,Before,MinSize)backend.ErrNotSupportedSearchOverlaytriggered by/from inbox, results render below the email list, Enter applies them as a temporary "Search Results" view, Esc clears/from "All Accounts" runs across every supported account;ErrNotSupportedis skipped silently so a mixed IMAP+JMAP+POP3 setup still surfaces results from the supported backendsinbox.search=/indefault_keybinds.jsonscreenshots/cmd/search_view/main.go+screenshots/search_demo.tapeso the demo regenerates without IMAP credentialsWhy?
Matches the spec from issue #508 directly:
Sibling issue #1131 added the ESEARCH + DSL prefix detail (
from:,to:,subject:,body:,since:,larger:, ESEARCH RETURN ALL with UID SEARCH fallback).The launch thread on r/coolgithubprojects had filtering as the very first comment ("By chance are there filtering options?"), so server-side search closes both the maintainer's and a user-visible gap.
Closes #508. Partially addresses #1131. Out of scope: #1129 (full-text index across folders).