feat: add support for marking emails as answered or forwarded and update UI accordingly

This commit is contained in:
Linus Rath
2026-03-23 15:45:37 +01:00
parent 0c1f182b6b
commit f7ee204262
11 changed files with 142 additions and 5 deletions
+24
View File
@@ -89,6 +89,22 @@ describe('groupEmailsByThread', () => {
expect(groupEmailsByThread(emails)[0].hasAttachment).toBe(true);
});
it('detects hasAnswered when an email has $answered', () => {
const emails = [
makeEmail({ id: 'e1', keywords: { $seen: true } }),
makeEmail({ id: 'e2', keywords: { $seen: true, $answered: true } }),
];
expect(groupEmailsByThread(emails)[0].hasAnswered).toBe(true);
});
it('detects hasForwarded when an email has $forwarded', () => {
const emails = [
makeEmail({ id: 'e1', keywords: { $seen: true } }),
makeEmail({ id: 'e2', keywords: { $seen: true, $forwarded: true } }),
];
expect(groupEmailsByThread(emails)[0].hasForwarded).toBe(true);
});
it('returns empty array for empty input', () => {
expect(groupEmailsByThread([])).toEqual([]);
});
@@ -110,6 +126,8 @@ describe('sortThreadGroups', () => {
hasUnread: false,
hasStarred: false,
hasAttachment: false,
hasAnswered: false,
hasForwarded: false,
emailCount: 1,
},
{
@@ -120,6 +138,8 @@ describe('sortThreadGroups', () => {
hasUnread: false,
hasStarred: false,
hasAttachment: false,
hasAnswered: false,
hasForwarded: false,
emailCount: 1,
},
];
@@ -169,6 +189,8 @@ describe('mergeThreadEmails', () => {
hasUnread: false,
hasStarred: false,
hasAttachment: false,
hasAnswered: false,
hasForwarded: false,
emailCount: 2,
};
const fetched = [
@@ -189,6 +211,8 @@ describe('mergeThreadEmails', () => {
hasUnread: false,
hasStarred: false,
hasAttachment: false,
hasAnswered: false,
hasForwarded: false,
emailCount: 1,
};
const fetched = [
+5
View File
@@ -216,6 +216,11 @@ export class DemoJMAPClient implements IJMAPClient {
if (email) email.keywords = { ...email.keywords, ...keywords };
}
async setKeyword(emailId: string, keyword: string): Promise<void> {
const email = this.data.emails.find(e => e.id === emailId);
if (email) email.keywords[keyword] = true;
}
async migrateKeyword(oldKeyword: string, newKeyword: string): Promise<number> {
let count = 0;
for (const email of this.data.emails) {
+1
View File
@@ -72,6 +72,7 @@ export interface IJMAPClient {
batchMarkAsRead(emailIds: string[], read?: boolean): Promise<void>;
toggleStar(emailId: string, starred: boolean): Promise<void>;
updateEmailKeywords(emailId: string, keywords: Record<string, boolean>): Promise<void>;
setKeyword(emailId: string, keyword: string): Promise<void>;
migrateKeyword(oldKeyword: string, newKeyword: string): Promise<number>;
deleteEmail(emailId: string): Promise<void>;
moveToTrash(emailId: string, trashMailboxId: string, accountId?: string): Promise<void>;
+13
View File
@@ -763,6 +763,19 @@ export class JMAPClient implements IJMAPClient {
]);
}
async setKeyword(emailId: string, keyword: string): Promise<void> {
await this.request([
["Email/set", {
accountId: this.accountId,
update: {
[emailId]: {
[`keywords/${keyword}`]: true,
},
},
}, "0"],
]);
}
async migrateKeyword(oldKeyword: string, newKeyword: string): Promise<number> {
// Query all email IDs that have the old keyword
const allIds: string[] = [];
+2
View File
@@ -142,6 +142,8 @@ export interface ThreadGroup {
hasUnread: boolean; // Any unread emails in thread
hasStarred: boolean; // Any starred emails in thread
hasAttachment: boolean; // Any email has attachment
hasAnswered: boolean; // Any email has been replied to
hasForwarded: boolean; // Any email has been forwarded
emailCount: number; // Total emails in thread
}
+8
View File
@@ -38,6 +38,8 @@ export function groupEmailsByThread(emails: Email[]): ThreadGroup[] {
const hasUnread = sortedEmails.some(e => !e.keywords?.$seen);
const hasStarred = sortedEmails.some(e => e.keywords?.$flagged);
const hasAttachment = sortedEmails.some(e => e.hasAttachment);
const hasAnswered = sortedEmails.some(e => e.keywords?.$answered);
const hasForwarded = sortedEmails.some(e => e.keywords?.$forwarded);
threadGroups.push({
threadId,
@@ -47,6 +49,8 @@ export function groupEmailsByThread(emails: Email[]): ThreadGroup[] {
hasUnread,
hasStarred,
hasAttachment,
hasAnswered,
hasForwarded,
emailCount: sortedEmails.length,
});
}
@@ -123,6 +127,8 @@ export function mergeThreadEmails(
const hasUnread = mergedEmails.some(e => !e.keywords?.$seen);
const hasStarred = mergedEmails.some(e => e.keywords?.$flagged);
const hasAttachment = mergedEmails.some(e => e.hasAttachment);
const hasAnswered = mergedEmails.some(e => e.keywords?.$answered);
const hasForwarded = mergedEmails.some(e => e.keywords?.$forwarded);
return {
threadId: existingGroup.threadId,
@@ -132,6 +138,8 @@ export function mergeThreadEmails(
hasUnread,
hasStarred,
hasAttachment,
hasAnswered,
hasForwarded,
emailCount: mergedEmails.length,
};
}