fix: fix inconsistent behavior with threading email messages in the inbox/folders
This commit is contained in:
@@ -335,6 +335,7 @@ export default function Home() {
|
||||
viewingAccountId,
|
||||
selectAccountMailbox,
|
||||
setViewingAccount,
|
||||
refreshCurrentMailbox,
|
||||
} = useEmailStore();
|
||||
|
||||
// Pro shell: populate per-account mailbox cache so the sidebar can render
|
||||
@@ -1168,7 +1169,26 @@ export default function Home() {
|
||||
}
|
||||
|
||||
// Refresh the current mailbox to update the UI
|
||||
if (!isScheduledView) await fetchEmails(client, selectedMailbox);
|
||||
if (!isScheduledView) {
|
||||
await refreshCurrentMailbox(client);
|
||||
// Re-fetch the replied thread's cross-folder data so the expanded
|
||||
// view shows the newly sent reply without collapsing.
|
||||
if (originalEmailId) {
|
||||
const emailState = useEmailStore.getState();
|
||||
const repliedEmail = emailState.emails.find(e => e.id === originalEmailId);
|
||||
if (repliedEmail?.threadId && emailState.expandedThreadIds.has(repliedEmail.threadId)) {
|
||||
const accountId = client.getAccountId();
|
||||
const fullEmails = await client.getThreadEmails(repliedEmail.threadId, accountId);
|
||||
if (fullEmails.length > 0) {
|
||||
useEmailStore.setState((state) => {
|
||||
const c = new Map(state.threadEmailsCache);
|
||||
c.set(repliedEmail.threadId!, fullEmails);
|
||||
return { threadEmailsCache: c };
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to send email:", error);
|
||||
}
|
||||
@@ -2201,7 +2221,22 @@ export default function Home() {
|
||||
}
|
||||
|
||||
// Refresh emails to show the sent reply
|
||||
await fetchEmails(client, selectedMailbox);
|
||||
await refreshCurrentMailbox(client);
|
||||
// Re-fetch the replied thread's cross-folder data so the expanded
|
||||
// view shows the newly sent reply without collapsing.
|
||||
const emailState = useEmailStore.getState();
|
||||
const repliedEmail = emailState.emails.find(e => e.id === originalEmailId);
|
||||
if (repliedEmail?.threadId && emailState.expandedThreadIds.has(repliedEmail.threadId)) {
|
||||
const accountId = client.getAccountId();
|
||||
const fullEmails = await client.getThreadEmails(repliedEmail.threadId, accountId);
|
||||
if (fullEmails.length > 0) {
|
||||
useEmailStore.setState((state) => {
|
||||
const c = new Map(state.threadEmailsCache);
|
||||
c.set(repliedEmail.threadId!, fullEmails);
|
||||
return { threadEmailsCache: c };
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Show loading state while checking auth
|
||||
|
||||
@@ -100,6 +100,8 @@ export function EmailList({
|
||||
isLoadingThread,
|
||||
toggleThreadExpansion,
|
||||
fetchThreadEmails,
|
||||
markThreadAsRead,
|
||||
threadEmailCounts,
|
||||
searchFilters,
|
||||
setSearchFilters,
|
||||
clearSearchFilters,
|
||||
@@ -110,9 +112,9 @@ export function EmailList({
|
||||
const disableThreading = useSettingsStore((state) => state.disableThreading);
|
||||
|
||||
const threadGroups = useMemo(() => {
|
||||
const groups = groupEmailsByThread(emails, disableThreading || isScheduledView);
|
||||
const groups = groupEmailsByThread(emails, disableThreading || isScheduledView, threadEmailCounts);
|
||||
return sortThreadGroups(groups);
|
||||
}, [emails, disableThreading, isScheduledView]);
|
||||
}, [emails, disableThreading, isScheduledView, threadEmailCounts]);
|
||||
|
||||
const { contextMenu, openContextMenu, closeContextMenu, menuRef } = useContextMenu<Email>();
|
||||
const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog();
|
||||
@@ -250,10 +252,12 @@ export function EmailList({
|
||||
if (!isExpanded && client) {
|
||||
toggleThreadExpansion(threadId);
|
||||
await fetchThreadEmails(client, threadId);
|
||||
// Mark all unread emails in this thread as read
|
||||
void markThreadAsRead(client, threadId);
|
||||
} else {
|
||||
toggleThreadExpansion(threadId);
|
||||
}
|
||||
}, [client, expandedThreadIds, toggleThreadExpansion, fetchThreadEmails]);
|
||||
}, [client, expandedThreadIds, toggleThreadExpansion, fetchThreadEmails, markThreadAsRead]);
|
||||
|
||||
// Range-based load more: trigger when last visible item is near the end.
|
||||
// Debounce to prevent rapid cascade when thread grouping reduces item
|
||||
|
||||
@@ -26,6 +26,7 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) {
|
||||
const client = useAuthStore((s) => s.client);
|
||||
const sendEmail = useEmailStore((s) => s.sendEmail);
|
||||
const fetchEmails = useEmailStore((s) => s.fetchEmails);
|
||||
const refreshCurrentMailbox = useEmailStore((s) => s.refreshCurrentMailbox);
|
||||
const fetchScheduledEmails = useEmailStore((s) => s.fetchScheduledEmails);
|
||||
const refreshScheduledMetadata = useEmailStore((s) => s.refreshScheduledMetadata);
|
||||
const selectedMailbox = useEmailStore((s) => s.selectedMailbox);
|
||||
@@ -92,7 +93,24 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) {
|
||||
|
||||
// Refresh the currently-active mail list so the new sent message /
|
||||
// updated keyword status shows up.
|
||||
await fetchEmails(client, selectedMailbox);
|
||||
await refreshCurrentMailbox(client);
|
||||
// Re-fetch the replied thread's cross-folder data so the expanded
|
||||
// view shows the newly sent reply without collapsing.
|
||||
if (data.sourceEmailId) {
|
||||
const emailState = useEmailStore.getState();
|
||||
const repliedEmail = emailState.emails.find(e => e.id === data.sourceEmailId);
|
||||
if (repliedEmail?.threadId && emailState.expandedThreadIds.has(repliedEmail.threadId)) {
|
||||
const accountId = client.getAccountId();
|
||||
const fullEmails = await client.getThreadEmails(repliedEmail.threadId, accountId);
|
||||
if (fullEmails.length > 0) {
|
||||
useEmailStore.setState((state) => {
|
||||
const c = new Map(state.threadEmailsCache);
|
||||
c.set(repliedEmail.threadId!, fullEmails);
|
||||
return { threadEmailsCache: c };
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
closeTab(tabIdRef.current);
|
||||
} catch (error) {
|
||||
console.error('Failed to send email:', error);
|
||||
|
||||
@@ -388,6 +388,16 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
return { id: threadId, emailIds: emails.map(e => e.id) };
|
||||
}
|
||||
|
||||
async getThreads(threadIds: string[]): Promise<Thread[]> {
|
||||
return threadIds
|
||||
.map(tid => {
|
||||
const emails = this.data.emails.filter(e => e.threadId === tid);
|
||||
if (emails.length === 0) return null;
|
||||
return { id: tid, emailIds: emails.map(e => e.id) };
|
||||
})
|
||||
.filter((t): t is Thread => t !== null);
|
||||
}
|
||||
|
||||
async getThreadEmails(threadId: string): Promise<Email[]> {
|
||||
return this.data.emails
|
||||
.filter(e => e.threadId === threadId)
|
||||
|
||||
@@ -117,6 +117,7 @@ export interface IJMAPClient {
|
||||
|
||||
// ── Threads ───────────────────────────────────────────────────
|
||||
getThread(threadId: string, accountId?: string): Promise<Thread | null>;
|
||||
getThreads(threadIds: string[], accountId?: string): Promise<Thread[]>;
|
||||
getThreadEmails(threadId: string, accountId?: string): Promise<Email[]>;
|
||||
|
||||
// ── Compose / Send ────────────────────────────────────────────
|
||||
|
||||
@@ -1904,6 +1904,24 @@ export class JMAPClient implements IJMAPClient {
|
||||
}
|
||||
}
|
||||
|
||||
async getThreads(threadIds: string[], accountId?: string): Promise<Thread[]> {
|
||||
if (threadIds.length === 0) return [];
|
||||
try {
|
||||
const targetAccountId = accountId || this.accountId;
|
||||
const response = await this.request([
|
||||
["Thread/get", { accountId: targetAccountId, ids: threadIds }, "0"],
|
||||
]);
|
||||
|
||||
if (response.methodResponses?.[0]?.[0] === "Thread/get") {
|
||||
return (response.methodResponses[0][1].list || []) as Thread[];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Failed to get threads:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getThreadEmails(threadId: string, accountId?: string): Promise<Email[]> {
|
||||
try {
|
||||
const targetAccountId = accountId || this.accountId;
|
||||
|
||||
+10
-2
@@ -5,8 +5,16 @@ import type { Email, ThreadGroup } from "./jmap/types";
|
||||
* Single-email threads are still returned as ThreadGroups with emailCount=1.
|
||||
* When disableThreading is true, each email is placed into its own group using
|
||||
* its message ID as the key, so the list shows individual messages.
|
||||
*
|
||||
* @param threadEmailCounts - Optional map of threadId → total email count across
|
||||
* all folders (from Thread/get). When provided, emailCount reflects the full
|
||||
* thread size rather than just the emails in the current folder.
|
||||
*/
|
||||
export function groupEmailsByThread(emails: Email[], disableThreading = false): ThreadGroup[] {
|
||||
export function groupEmailsByThread(
|
||||
emails: Email[],
|
||||
disableThreading = false,
|
||||
threadEmailCounts?: Map<string, number>,
|
||||
): ThreadGroup[] {
|
||||
if (!emails || emails.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -53,7 +61,7 @@ export function groupEmailsByThread(emails: Email[], disableThreading = false):
|
||||
hasAttachment,
|
||||
hasAnswered,
|
||||
hasForwarded,
|
||||
emailCount: sortedEmails.length,
|
||||
emailCount: threadEmailCounts?.get(threadId) ?? sortedEmails.length,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+207
-5
@@ -60,6 +60,8 @@ interface EmailStore {
|
||||
expandedThreadIds: Set<string>;
|
||||
threadEmailsCache: Map<string, Email[]>;
|
||||
isLoadingThread: string | null;
|
||||
// Full thread email counts (from Thread/get across all folders)
|
||||
threadEmailCounts: Map<string, number>;
|
||||
|
||||
// Keyword/tag filter
|
||||
selectedKeyword: string | null;
|
||||
@@ -199,6 +201,8 @@ interface EmailStore {
|
||||
fetchThreadEmails: (client: IJMAPClient, threadId: string) => Promise<Email[]>;
|
||||
collapseAllThreads: () => void;
|
||||
updateThreadCache: (threadId: string, emails: Email[]) => void;
|
||||
fetchThreadEmailCounts: (client: IJMAPClient) => Promise<void>;
|
||||
markThreadAsRead: (client: IJMAPClient, threadId: string) => Promise<void>;
|
||||
|
||||
// Mailbox management
|
||||
createMailbox: (client: IJMAPClient, name: string, parentId?: string) => Promise<void>;
|
||||
@@ -519,6 +523,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
expandedThreadIds: new Set(),
|
||||
threadEmailsCache: new Map(),
|
||||
isLoadingThread: null,
|
||||
threadEmailCounts: new Map(),
|
||||
|
||||
// Keyword/tag filter
|
||||
selectedKeyword: null,
|
||||
@@ -565,6 +570,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
selectedKeyword: null,
|
||||
expandedThreadIds: new Set(),
|
||||
threadEmailsCache: new Map(),
|
||||
threadEmailCounts: new Map(),
|
||||
isLoadingThread: null,
|
||||
}),
|
||||
fetchAccountMailboxes: async (client, accountId) => {
|
||||
@@ -595,6 +601,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
selectedEmailIds: new Set(),
|
||||
expandedThreadIds: new Set(),
|
||||
threadEmailsCache: new Map(),
|
||||
threadEmailCounts: new Map(),
|
||||
}),
|
||||
fetchTagCounts: async (client) => {
|
||||
try {
|
||||
@@ -617,6 +624,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
selectedKeyword: null,
|
||||
expandedThreadIds: new Set(),
|
||||
threadEmailsCache: new Map(),
|
||||
threadEmailCounts: new Map(),
|
||||
isLoadingThread: null,
|
||||
}),
|
||||
setLoading: (loading) => set({ isLoading: loading }),
|
||||
@@ -785,8 +793,14 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
emails: annotateScheduledEmails(result.emails, get().scheduledSubmissionByEmailId),
|
||||
hasMoreEmails: result.hasMore,
|
||||
totalEmails: result.total,
|
||||
// Clear thread caches since the email list was fully replaced
|
||||
threadEmailsCache: new Map(),
|
||||
expandedThreadIds: new Set(),
|
||||
isLoadingThread: null,
|
||||
isLoading: false
|
||||
});
|
||||
// Fetch full thread counts in the background (non-blocking)
|
||||
void get().fetchThreadEmailCounts(client);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch emails:', error);
|
||||
set({
|
||||
@@ -923,6 +937,10 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
totalEmails: result.total,
|
||||
isLoadingMore: false
|
||||
});
|
||||
// Fetch full thread counts for newly loaded threads in the background
|
||||
if (newEmails.length > 0) {
|
||||
void get().fetchThreadEmailCounts(client);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load more emails:', error);
|
||||
set({
|
||||
@@ -1214,7 +1232,23 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
? { ...state.selectedEmail, keywords: { ...state.selectedEmail.keywords, $seen: read } }
|
||||
: state.selectedEmail,
|
||||
mailboxes: updatedMailboxes,
|
||||
processingReadStatus: newProcessingSet
|
||||
processingReadStatus: newProcessingSet,
|
||||
// Also update threadEmailsCache so expanded dropdowns reflect the change
|
||||
threadEmailsCache: (() => {
|
||||
let updated = false;
|
||||
const newCache = new Map(state.threadEmailsCache);
|
||||
for (const [tid, cachedEmails] of newCache) {
|
||||
const idx = cachedEmails.findIndex(e => e.id === emailId);
|
||||
if (idx !== -1) {
|
||||
newCache.set(tid, cachedEmails.map((e, i) =>
|
||||
i === idx ? { ...e, keywords: { ...e.keywords, $seen: read } } : e
|
||||
));
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return updated ? newCache : state.threadEmailsCache;
|
||||
})(),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -2327,11 +2361,74 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
// hasMore should reflect whether there are still more emails beyond
|
||||
// what we have loaded, using the fresh total from the server.
|
||||
const hasMore = merged.length < (result.total || 0);
|
||||
set({
|
||||
emails: merged,
|
||||
hasMoreEmails: hasMore,
|
||||
totalEmails: result.total,
|
||||
|
||||
// Invalidate thread email caches for threads whose composition changed
|
||||
// so expanded threads pick up new/removed emails.
|
||||
const prevThreadIds = new Set(currentEmails.map(e => e.threadId));
|
||||
const nextThreadIds = new Set(merged.map(e => e.threadId));
|
||||
const changedThreadIds = new Set<string>();
|
||||
for (const tid of prevThreadIds) {
|
||||
if (!nextThreadIds.has(tid)) changedThreadIds.add(tid);
|
||||
}
|
||||
for (const tid of nextThreadIds) {
|
||||
if (!prevThreadIds.has(tid)) changedThreadIds.add(tid);
|
||||
}
|
||||
// Also check threads where the set of email IDs changed
|
||||
const prevEmailsByThread = new Map<string, Set<string>>();
|
||||
for (const e of currentEmails) {
|
||||
if (!prevEmailsByThread.has(e.threadId)) prevEmailsByThread.set(e.threadId, new Set());
|
||||
prevEmailsByThread.get(e.threadId)!.add(e.id);
|
||||
}
|
||||
const nextEmailsByThread = new Map<string, Set<string>>();
|
||||
for (const e of merged) {
|
||||
if (!nextEmailsByThread.has(e.threadId)) nextEmailsByThread.set(e.threadId, new Set());
|
||||
nextEmailsByThread.get(e.threadId)!.add(e.id);
|
||||
}
|
||||
for (const [tid, nextIds] of nextEmailsByThread) {
|
||||
const prevIds = prevEmailsByThread.get(tid);
|
||||
if (!prevIds || prevIds.size !== nextIds.size) {
|
||||
changedThreadIds.add(tid);
|
||||
} else {
|
||||
for (const id of nextIds) {
|
||||
if (!prevIds.has(id)) { changedThreadIds.add(tid); break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const newCache = new Map(state.threadEmailsCache);
|
||||
for (const tid of changedThreadIds) {
|
||||
newCache.delete(tid);
|
||||
}
|
||||
return {
|
||||
emails: merged,
|
||||
hasMoreEmails: hasMore,
|
||||
totalEmails: result.total,
|
||||
threadEmailsCache: newCache,
|
||||
};
|
||||
});
|
||||
|
||||
// Re-fetch cross-folder thread data for any currently expanded threads
|
||||
// so they show the complete conversation (not just current-folder emails).
|
||||
const expandedNow = get().expandedThreadIds;
|
||||
if (expandedNow.size > 0) {
|
||||
const effectiveClient2 = resolveActionClient(client);
|
||||
const accountId = effectiveClient2.getAccountId();
|
||||
for (const tid of expandedNow) {
|
||||
void effectiveClient2.getThreadEmails(tid, accountId).then((fullEmails) => {
|
||||
if (fullEmails.length > 0) {
|
||||
set((state) => {
|
||||
const c = new Map(state.threadEmailsCache);
|
||||
c.set(tid, fullEmails);
|
||||
return { threadEmailsCache: c };
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch full thread counts in the background (non-blocking)
|
||||
void get().fetchThreadEmailCounts(client);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh current mailbox:', error);
|
||||
@@ -2401,6 +2498,90 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
markThreadAsRead: async (client, threadId) => {
|
||||
const state = get();
|
||||
const threadEmails = state.threadEmailsCache.get(threadId) ?? [];
|
||||
const mainEmails = state.emails.filter(e => e.threadId === threadId);
|
||||
|
||||
// Combine unique emails from both sources
|
||||
const allEmailMap = new Map<string, Email>();
|
||||
for (const e of mainEmails) allEmailMap.set(e.id, e);
|
||||
for (const e of threadEmails) allEmailMap.set(e.id, e);
|
||||
|
||||
const unreadIds = Array.from(allEmailMap.values())
|
||||
.filter(e => !e.keywords?.$seen)
|
||||
.map(e => e.id);
|
||||
|
||||
if (unreadIds.length === 0) return;
|
||||
|
||||
// Group by account for unified view support
|
||||
const emailsById = new Map<string, Email>();
|
||||
for (const e of allEmailMap.values()) emailsById.set(e.id, e);
|
||||
|
||||
// Group unread IDs by account client
|
||||
const groups = new Map<IJMAPClient, string[]>();
|
||||
for (const id of unreadIds) {
|
||||
const email = emailsById.get(id)!;
|
||||
const { client: actionClient } = resolveEmailActionContext(email, client);
|
||||
if (!groups.has(actionClient)) groups.set(actionClient, []);
|
||||
groups.get(actionClient)!.push(id);
|
||||
}
|
||||
|
||||
// Mark all as read on the server
|
||||
try {
|
||||
await Promise.all(
|
||||
Array.from(groups.entries()).map(([actionClient, emailIds]) =>
|
||||
actionClient.batchMarkAsRead(emailIds, true)
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to mark thread as read:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update local state
|
||||
set((state) => {
|
||||
const unreadSet = new Set(unreadIds);
|
||||
|
||||
const updatedEmails = state.emails.map(e =>
|
||||
unreadSet.has(e.id) ? { ...e, keywords: { ...e.keywords, $seen: true } } : e
|
||||
);
|
||||
|
||||
// Update threadEmailsCache
|
||||
const newCache = new Map(state.threadEmailsCache);
|
||||
const cached = newCache.get(threadId);
|
||||
if (cached) {
|
||||
newCache.set(threadId, cached.map(e =>
|
||||
unreadSet.has(e.id) ? { ...e, keywords: { ...e.keywords, $seen: true } } : e
|
||||
));
|
||||
}
|
||||
|
||||
// Update mailbox unread counters
|
||||
const affectedEmails = state.emails.filter(e => unreadSet.has(e.id));
|
||||
const updatedMailboxes = state.mailboxes.map(mailbox => {
|
||||
let delta = 0;
|
||||
for (const email of affectedEmails) {
|
||||
if (email.mailboxIds?.[mailbox.id]) delta -= 1;
|
||||
}
|
||||
if (delta === 0) return mailbox;
|
||||
return {
|
||||
...mailbox,
|
||||
unreadEmails: Math.max(0, mailbox.unreadEmails + delta),
|
||||
unreadThreads: Math.max(0, mailbox.unreadThreads + delta),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
emails: updatedEmails,
|
||||
threadEmailsCache: newCache,
|
||||
mailboxes: updatedMailboxes,
|
||||
selectedEmail: state.selectedEmail && unreadSet.has(state.selectedEmail.id)
|
||||
? { ...state.selectedEmail, keywords: { ...state.selectedEmail.keywords, $seen: true } }
|
||||
: state.selectedEmail,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
collapseAllThreads: () => {
|
||||
set({
|
||||
expandedThreadIds: new Set(),
|
||||
@@ -2414,6 +2595,27 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
set({ threadEmailsCache: newCache });
|
||||
},
|
||||
|
||||
fetchThreadEmailCounts: async (client) => {
|
||||
const { emails } = get();
|
||||
if (emails.length === 0) return;
|
||||
|
||||
const uniqueThreadIds = [...new Set(emails.map(e => e.threadId).filter(Boolean))];
|
||||
if (uniqueThreadIds.length === 0) return;
|
||||
|
||||
try {
|
||||
const effectiveClient = resolveActionClient(client);
|
||||
const threads = await effectiveClient.getThreads(uniqueThreadIds);
|
||||
|
||||
const newCounts = new Map(get().threadEmailCounts);
|
||||
for (const thread of threads) {
|
||||
newCounts.set(thread.id, thread.emailIds?.length ?? 0);
|
||||
}
|
||||
set({ threadEmailCounts: newCounts });
|
||||
} catch {
|
||||
// Non-critical — fall back to inbox-only counts
|
||||
}
|
||||
},
|
||||
|
||||
// Mailbox management
|
||||
createMailbox: async (client, name, parentId) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user