diff --git a/README.md b/README.md
index 50f50df2..1a0666be 100644
--- a/README.md
+++ b/README.md
@@ -183,14 +183,6 @@ npm start
**Settings**
-
-
-
-
-
-**Sidebar Navigation**
-
-
|
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
index 17a8437d..f585000f 100644
--- a/app/[locale]/page.tsx
+++ b/app/[locale]/page.tsx
@@ -67,6 +67,8 @@ export default function Home() {
toggleStar,
moveToMailbox,
searchEmails,
+ searchQuery,
+ setSearchQuery,
isLoading,
isLoadingEmail,
setLoadingEmail,
@@ -469,7 +471,12 @@ export default function Home() {
}
if (client) {
- await fetchEmails(client, mailboxId);
+ // If there's an active search, re-run it in the new mailbox
+ if (searchQuery) {
+ await searchEmails(client, searchQuery);
+ } else {
+ await fetchEmails(client, mailboxId);
+ }
}
};
@@ -483,6 +490,13 @@ export default function Home() {
await searchEmails(client, query);
};
+ const handleClearSearch = async () => {
+ setSearchQuery("");
+ if (client && selectedMailbox) {
+ await fetchEmails(client, selectedMailbox);
+ }
+ };
+
const handleDownloadAttachment = async (blobId: string, name: string, type?: string) => {
if (!client) return;
@@ -645,6 +659,8 @@ export default function Home() {
}}
onLogout={handleLogout}
onSearch={handleSearch}
+ onClearSearch={handleClearSearch}
+ activeSearchQuery={searchQuery}
quota={quota}
isPushConnected={isPushConnected}
/>
diff --git a/components/email/email-list.tsx b/components/email/email-list.tsx
index cfbc8dbf..5dacae7b 100644
--- a/components/email/email-list.tsx
+++ b/components/email/email-list.tsx
@@ -60,6 +60,7 @@ export function EmailList({
loadMoreEmails,
hasMoreEmails,
isLoadingMore,
+ totalEmails,
mailboxes,
selectedMailbox,
expandedThreadIds,
@@ -260,7 +261,13 @@ export function EmailList({
)}
- {isLoading ? 'Loading...' : threadGroups.length > 0 ? `${threadGroups.length} conversations` : 'No conversations'}
+ {isLoading ? 'Loading...' : threadGroups.length > 0
+ ? (totalEmails > threadGroups.length
+ ? `${threadGroups.length} of ${totalEmails} conversations`
+ : hasMoreEmails
+ ? `${threadGroups.length}+ conversations`
+ : `${threadGroups.length} conversations`)
+ : 'No conversations'}
diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx
index fad0aa83..4933c87d 100644
--- a/components/layout/sidebar.tsx
+++ b/components/layout/sidebar.tsx
@@ -24,6 +24,7 @@ import {
ChevronUp,
Users,
User,
+ X,
} from "lucide-react";
import { cn, buildMailboxTree, MailboxNode, formatFileSize } from "@/lib/utils";
import { Mailbox } from "@/lib/jmap/types";
@@ -37,6 +38,8 @@ interface SidebarProps {
onCompose?: () => void;
onLogout?: () => void;
onSearch?: (query: string) => void;
+ onClearSearch?: () => void;
+ activeSearchQuery?: string;
quota?: { used: number; total: number } | null;
isPushConnected?: boolean;
className?: string;
@@ -206,6 +209,8 @@ export function Sidebar({
onCompose,
onLogout,
onSearch,
+ onClearSearch,
+ activeSearchQuery = "",
quota,
isPushConnected = false,
className,
@@ -215,6 +220,11 @@ export function Sidebar({
const [expandedFolders, setExpandedFolders] = useState>(new Set());
const [showMenu, setShowMenu] = useState(false);
const t = useTranslations('sidebar');
+
+ // Sync local search query with store's active search query
+ useEffect(() => {
+ setSearchQuery(activeSearchQuery);
+ }, [activeSearchQuery]);
const params = useParams();
const router = useRouter();
@@ -334,9 +344,22 @@ export function Sidebar({
placeholder={t("search_placeholder")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
- className="pl-9"
+ className={cn("pl-9", searchQuery && "pr-8")}
data-search-input
/>
+ {searchQuery && (
+
+ )}
)}
diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts
index bb25dc34..313ae604 100644
--- a/lib/jmap/client.ts
+++ b/lib/jmap/client.ts
@@ -753,19 +753,27 @@ export class JMAPClient {
]);
}
- async searchEmails(query: string, limit: number = 50): Promise {
+ async searchEmails(query: string, mailboxId?: string, accountId?: string, limit: number = 50, position: number = 0): Promise<{ emails: Email[], hasMore: boolean, total: number }> {
try {
+ // Use provided accountId or fallback to primary account
+ const targetAccountId = accountId || this.accountId;
+
+ // Build filter with text search, optionally scoped to a mailbox
+ const filter: Record = { text: query };
+ if (mailboxId) {
+ filter.inMailbox = mailboxId;
+ }
+
const response = await this.request([
["Email/query", {
- accountId: this.accountId,
- filter: {
- text: query,
- },
+ accountId: targetAccountId,
+ filter: filter,
sort: [{ property: "receivedAt", isAscending: false }],
limit: limit,
+ position: position,
}, "0"],
["Email/get", {
- accountId: this.accountId,
+ accountId: targetAccountId,
"#ids": {
resultOf: "0",
name: "Email/query",
@@ -788,14 +796,21 @@ export class JMAPClient {
}, "1"],
]);
- if (response.methodResponses?.[1]?.[0] === "Email/get") {
- return response.methodResponses[1][1].list || [];
- }
+ const queryResponse = response.methodResponses?.[0]?.[1];
+ const emails = response.methodResponses?.[1]?.[1]?.list || [];
- return [];
+ // Stalwart doesn't always return 'total', so we use a different strategy:
+ // If we got exactly 'limit' emails, there might be more
+ // If we got fewer, we've reached the end
+ const total = queryResponse?.total || 0;
+ const hasMore = total > 0
+ ? (position + emails.length) < total // Use total if available
+ : emails.length === limit; // Otherwise, check if we got a full page
+
+ return { emails, hasMore, total };
} catch (error) {
console.error('Search failed:', error);
- return [];
+ return { emails: [], hasMore: false, total: 0 };
}
}
diff --git a/screenshots/07-inbox-sidebar.png b/screenshots/07-inbox-sidebar.png
deleted file mode 100644
index fae09583..00000000
Binary files a/screenshots/07-inbox-sidebar.png and /dev/null differ
diff --git a/stores/email-store.ts b/stores/email-store.ts
index 88277632..6a5ca4ae 100644
--- a/stores/email-store.ts
+++ b/stores/email-store.ts
@@ -200,25 +200,39 @@ export const useEmailStore = create((set, get) => ({
},
loadMoreEmails: async (client) => {
- const { isLoadingMore, hasMoreEmails, emails, selectedMailbox } = get();
+ const { isLoadingMore, hasMoreEmails, emails, selectedMailbox, searchQuery } = get();
// Don't load if already loading or no more emails
if (isLoadingMore || !hasMoreEmails) return;
set({ isLoadingMore: true, error: null });
try {
- // Find the mailbox to get its accountId (for shared folder support)
- const mailboxes = get().mailboxes;
- const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
- // Only pass accountId for shared mailboxes, not for primary account
- const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
- // Use originalId for JMAP queries (shared mailboxes use namespaced IDs in the store)
- const jmapMailboxId = mailbox?.originalId || selectedMailbox;
-
// Get emails per page from settings
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
- const result = await client.getEmails(jmapMailboxId, accountId, emailsPerPage, emails.length);
+ let result;
+
+ // Check if we're in search mode
+ if (searchQuery) {
+ // Load more search results (scoped to current mailbox)
+ const mailboxes = get().mailboxes;
+ const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
+ const jmapMailboxId = mailbox?.originalId || selectedMailbox;
+ // Only pass accountId for shared mailboxes
+ const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
+ result = await client.searchEmails(searchQuery, jmapMailboxId, accountId, emailsPerPage, emails.length);
+ } else {
+ // Load more from mailbox
+ // Find the mailbox to get its accountId (for shared folder support)
+ const mailboxes = get().mailboxes;
+ const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
+ // Only pass accountId for shared mailboxes, not for primary account
+ const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
+ // Use originalId for JMAP queries (shared mailboxes use namespaced IDs in the store)
+ const jmapMailboxId = mailbox?.originalId || selectedMailbox;
+
+ result = await client.getEmails(jmapMailboxId, accountId, emailsPerPage, emails.length);
+ }
set({
emails: [...emails, ...result.emails],
@@ -565,8 +579,24 @@ export const useEmailStore = create((set, get) => ({
searchEmails: async (client, query) => {
set({ isLoading: true, error: null, searchQuery: query, emails: [], hasMoreEmails: false, totalEmails: 0 }); // Clear emails for loading state
try {
- const emails = await client.searchEmails(query);
- set({ emails, isLoading: false, hasMoreEmails: false, totalEmails: emails.length });
+ // Get the current mailbox to scope the search
+ const selectedMailbox = get().selectedMailbox;
+ const mailboxes = get().mailboxes;
+ const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
+ // Use originalId for shared mailboxes
+ const jmapMailboxId = mailbox?.originalId || selectedMailbox;
+ // Only pass accountId for shared mailboxes, not for primary account
+ const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
+
+ // Get emails per page from settings
+ const emailsPerPage = useSettingsStore.getState().emailsPerPage;
+ const result = await client.searchEmails(query, jmapMailboxId, accountId, emailsPerPage, 0);
+ set({
+ emails: result.emails,
+ hasMoreEmails: result.hasMore,
+ totalEmails: result.total,
+ isLoading: false
+ });
} catch (error) {
set({
error: error instanceof Error ? error.message : "Failed to search emails",