This commit is contained in:
Linus Rath
2026-07-07 20:36:05 +02:00
44 changed files with 565 additions and 86 deletions
+3 -1
View File
@@ -78,7 +78,9 @@ export interface IJMAPClient {
deleteMailbox(mailboxId: string): Promise<void>;
// ── Emails ────────────────────────────────────────────────────
getEmails(mailboxId?: string, accountId?: string, limit?: number, position?: number, hasKeyword?: string): Promise<{ emails: Email[]; hasMore: boolean; total: number }>;
// `pinnedFirst` sorts emails carrying the $pinned keyword to the top
// (server-side hasKeyword sort comparator, RFC 8621), then receivedAt desc.
getEmails(mailboxId?: string, accountId?: string, limit?: number, position?: number, hasKeyword?: string, pinnedFirst?: boolean): Promise<{ emails: Email[]; hasMore: boolean; total: number }>;
getEmailsInMailbox(mailboxId: string): Promise<Email[]>;
getEmail(emailId: string, accountId?: string): Promise<Email | null>;
getTagCounts(tagIds: string[]): Promise<Record<string, { total: number; unread: number }>>;
+13 -2
View File
@@ -1062,7 +1062,7 @@ export class JMAPClient implements IJMAPClient {
}
}
async getEmails(mailboxId?: string, accountId?: string, limit: number = 50, position: number = 0, hasKeyword?: string): Promise<{ emails: Email[], hasMore: boolean, total: number }> {
async getEmails(mailboxId?: string, accountId?: string, limit: number = 50, position: number = 0, hasKeyword?: string, pinnedFirst?: boolean): Promise<{ emails: Email[], hasMore: boolean, total: number }> {
try {
const targetAccountId = accountId || this.accountId;
const filter: { inMailbox?: string; hasKeyword?: string } = {};
@@ -1072,12 +1072,20 @@ export class JMAPClient implements IJMAPClient {
if (hasKeyword) {
filter.hasKeyword = hasKeyword;
}
// Pinned-first uses the hasKeyword sort comparator (RFC 8621 §4.4.2);
// every page of a view must use the same sort or pagination tears.
const sort = pinnedFirst
? [
{ property: "hasKeyword", keyword: "$pinned", isAscending: false },
{ property: "receivedAt", isAscending: false },
]
: [{ property: "receivedAt", isAscending: false }];
const response = await this.request([
["Email/query", {
accountId: targetAccountId,
filter,
sort: [{ property: "receivedAt", isAscending: false }],
sort,
limit,
position,
calculateTotal: true,
@@ -1096,7 +1104,10 @@ export class JMAPClient implements IJMAPClient {
const emails = (getResponse.list || []) as Email[];
// Sort client-side as safety net - some servers may not honour
// the query sort for large mailboxes without additional filters.
// Must mirror the query sort, or it would undo the pinned-first order.
const pinRank = (e: Email) => (pinnedFirst && e.keywords?.['$pinned'] ? 1 : 0);
emails.sort((a: Email, b: Email) =>
pinRank(b) - pinRank(a) ||
new Date(b.receivedAt).getTime() - new Date(a.receivedAt).getTime()
);
const total = queryResponse?.total || 0;
+1
View File
@@ -217,6 +217,7 @@ export interface ThreadGroup {
participantNames: string[];// Unique participant names
hasUnread: boolean; // Any unread emails in thread
hasStarred: boolean; // Any starred emails in thread
hasPinned: boolean; // Any pinned emails in thread ($pinned keyword)
hasAttachment: boolean; // Any email has attachment
hasAnswered: boolean; // Any email has been replied to
hasForwarded: boolean; // Any email has been forwarded