From 83389e93fc02d7fa56e1d37a8c791a3646020d5f Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Mon, 25 May 2026 20:04:52 +0530 Subject: [PATCH] fixed multiple recepients / attachments in email issue --- apps/x/apps/renderer/src/App.css | 1 + .../renderer/src/components/email-view.tsx | 5 +++- .../packages/core/src/knowledge/sync_gmail.ts | 30 ++++++++++++++----- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/apps/x/apps/renderer/src/App.css b/apps/x/apps/renderer/src/App.css index 64bd9779..46763d5c 100644 --- a/apps/x/apps/renderer/src/App.css +++ b/apps/x/apps/renderer/src/App.css @@ -536,6 +536,7 @@ .gmail-message-from span, .gmail-message-to, +.gmail-message-cc, .gmail-message-date { color: var(--gm-text-muted); font-size: 12px; diff --git a/apps/x/apps/renderer/src/components/email-view.tsx b/apps/x/apps/renderer/src/components/email-view.tsx index 32f022a3..deed545c 100644 --- a/apps/x/apps/renderer/src/components/email-view.tsx +++ b/apps/x/apps/renderer/src/components/email-view.tsx @@ -998,7 +998,10 @@ function ThreadDetail({ {isExpanded ? ( -
to {message.to || 'me'}
+ <> +
to {message.to || 'me'}
+ {message.cc &&
cc {message.cc}
} + ) : (
{snippet(message.body)}
)} diff --git a/apps/x/packages/core/src/knowledge/sync_gmail.ts b/apps/x/packages/core/src/knowledge/sync_gmail.ts index 1f98736a..6b131a5d 100644 --- a/apps/x/packages/core/src/knowledge/sync_gmail.ts +++ b/apps/x/packages/core/src/knowledge/sync_gmail.ts @@ -31,9 +31,16 @@ const MAX_THREADS_IN_DIGEST = 10; const RECENT_BACKFILL_INTERVAL_MS = 15 * 60 * 1000; const nhm = new NodeHtmlMarkdown(); +// Bump whenever snapshot-building logic changes in a way that should invalidate +// previously cached snapshots (e.g. attachment / recipient parsing fixes). The +// short-circuit in buildAndCacheSnapshot only reuses a cache whose version matches, +// so stale entries are transparently rebuilt on the next sync. +const SNAPSHOT_PARSER_VERSION = 2; + interface SnapshotCacheEntry { historyId: string; fetchedAt: string; + parserVersion?: number; snapshot: GmailThreadSnapshot; } @@ -56,6 +63,7 @@ function writeCachedSnapshot(threadId: string, historyId: string, snapshot: Gmai const entry: SnapshotCacheEntry = { historyId, fetchedAt: new Date().toISOString(), + parserVersion: SNAPSHOT_PARSER_VERSION, snapshot, }; fs.writeFileSync(cachePath(threadId), JSON.stringify(entry), 'utf-8'); @@ -308,19 +316,24 @@ interface ExtractedAttachment { * saveAttachment / processThread, so the renderer can hand them to * shell.openPath via the existing IPC. */ -function extractAttachments(msgId: string, payload: gmail.Schema$MessagePart): ExtractedAttachment[] { +function extractAttachments(msgId: string, payload: gmail.Schema$MessagePart, html?: string): ExtractedAttachment[] { const out: ExtractedAttachment[] = []; const walk = (part: gmail.Schema$MessagePart): void => { const filename = part.filename; const attId = part.body?.attachmentId; if (filename && attId) { - // Exclude only true inline images (image/* with a Content-ID, which - // get baked into bodyHtml as data URLs by inlineCidImages). Other - // parts with Content-ID — PDFs, .log files, .ics, etc. — are real - // attachments; Gmail just stamps Content-ID on most parts. - const cid = part.headers?.find(h => h.name?.toLowerCase() === 'content-id')?.value; + // Exclude only images that are genuinely inline — i.e. their Content-ID + // is actually referenced via `cid:` in the HTML body, so inlineCidImages + // already baked them in as data URLs. Gmail stamps a Content-ID on most + // parts (including real, separately-attached images like screenshots or + // scanned docs), so a Content-ID alone must NOT exclude an attachment; + // otherwise attached images silently disappear from the thread view. + const cidRaw = part.headers?.find(h => h.name?.toLowerCase() === 'content-id')?.value; + const cid = cidRaw?.replace(/^<|>$/g, '').trim(); const mime = part.mimeType || ''; - const isInlineImage = !!cid && mime.startsWith('image/'); + const referencedInHtml = !!cid && !!html + && new RegExp(`cid:${cid.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i').test(html); + const isInlineImage = mime.startsWith('image/') && referencedInHtml; if (!isInlineImage) { const safeName = `${msgId}_${cleanFilename(filename)}`; out.push({ @@ -577,6 +590,7 @@ async function buildAndCacheSnapshot( threadData.historyId && cached && cached.historyId === threadData.historyId && + cached.parserVersion === SNAPSHOT_PARSER_VERSION && cached.snapshot.importance ) { return cached.snapshot; @@ -602,7 +616,7 @@ async function buildAndCacheSnapshot( } } const isDraft = msg.labelIds?.includes('DRAFT') ?? false; - const attachments = msg.payload && msg.id ? extractAttachments(msg.id, msg.payload) : []; + const attachments = msg.payload && msg.id ? extractAttachments(msg.id, msg.payload, parts.html) : []; return { id: msg.id || undefined, from: headerValue(headers, 'From') || 'Unknown',