Skip to content

feat(gmail): add reply, forward, attachment, and message management operations#44

Merged
AojdevStudio merged 1 commit intomainfrom
feature/gdrive-12-gmail-operations
Mar 4, 2026
Merged

feat(gmail): add reply, forward, attachment, and message management operations#44
AojdevStudio merged 1 commit intomainfrom
feature/gdrive-12-gmail-operations

Conversation

@AojdevStudio
Copy link
Copy Markdown
Owner

@AojdevStudio AojdevStudio commented Mar 4, 2026

Summary

Implements GDRIVE-12: Complete human-parity email operations for the Gmail service.

  • replyToMessage — Correctly threaded replies using MIME Message-ID, In-Reply-To, and References headers (fixes unreliable threading with mailing lists/ConvertKit)
  • replyAllToMessage — Reply-all with automatic recipient deduplication and self-exclusion
  • forwardMessage — Forward with quoted original content, optional custom body
  • listAttachments / downloadAttachment / sendWithAttachments — Full attachment lifecycle with multipart MIME support
  • trashMessage / untrashMessage / deleteMessage — Message lifecycle management with safety guard (safetyAcknowledged: true required for permanent delete)
  • markAsRead / markAsUnread / archiveMessage — Convenience wrappers for common label modifications

Key Technical Decisions

  • replyToMessage fetches original with format: 'metadata' for efficient threading header extraction
  • New buildMultipartMessage utility in utils.ts for multipart/mixed MIME encoding
  • All 12 operations registered through the full SDK pipeline (types → operation → index → runtime → spec → SDK types)
  • Zero changes to existing operations — purely additive

Test Plan

  • 54 new tests across 4 test files (reply, forward, attachments, manage)
  • Full suite: 42 suites, 536 tests passing, 0 failures
  • Edge cases: missing headers, mailing lists, self-exclusion, safety guards

Resolves GDRIVE-12

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added email reply functionality with threading support, including reply-all with automatic recipient deduplication
    • Added email forwarding with quoted original content
    • Added attachment management: list attachments, download attachments, and send emails with attached files
    • Added message management operations: trash, untrash, permanently delete, mark as read/unread, and archive messages
    • Version bump to 3.3.0

…perations

Implements GDRIVE-12: Complete human-parity email operations including:
- replyToMessage with proper MIME threading headers
- replyAllToMessage with recipient deduplication
- forwardMessage with quoted original content
- listAttachments, downloadAttachment, sendWithAttachments
- trashMessage, untrashMessage, deleteMessage (with safety guard)
- markAsRead, markAsUnread, archiveMessage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@linear
Copy link
Copy Markdown

linear Bot commented Mar 4, 2026

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

This pull request expands the Gmail SDK module with comprehensive support for message threading (reply, reply-all), forwarding, attachment handling, and message management operations. Twelve new functions are introduced with corresponding type definitions, SDK runtime integration, and extensive test coverage. The module version is bumped from 3.2.0 to 3.3.0.

Changes

Cohort / File(s) Summary
Gmail Operations Implementation
src/modules/gmail/reply.ts, src/modules/gmail/forward.ts, src/modules/gmail/attachments.ts, src/modules/gmail/manage.ts
Four new modules implementing 12 Gmail operations: replyToMessage and replyAllToMessage with MIME threading headers and recipient handling; forwardMessage with subject/body quoting; listAttachments, downloadAttachment, and sendWithAttachments for attachment management; trashMessage, untrashMessage, deleteMessage, markAsRead, markAsUnread, and archiveMessage for message lifecycle. All include cache invalidation, performance tracking, and error handling.
Utility Functions & Type Definitions
src/modules/gmail/utils.ts, src/modules/gmail/types.ts
buildMultipartMessage function for RFC 2822 compliant multipart/mixed MIME construction with attachment support; AttachmentData interface. New types added: 26 interfaces (Options/Result pairs) for all new operations covering reply, forward, attachments, and management.
Module & SDK Exports
src/modules/gmail/index.ts, src/sdk/types.ts, src/sdk/spec.ts, src/sdk/runtime.ts
Version bump to 3.3.0. Exports 12 new functions and 14 new type groups from gmail module. SDK runtime wrappers added for rate limiting and lazy loading of all 12 new operations. SDK spec updated with detailed signatures, descriptions, examples, and schemas for each operation. SDK type interface extended with 12 new methods.
Test Suite
src/__tests__/sdk/runtime-rate-limiter-scope.test.ts, src/modules/gmail/__tests__/reply.test.ts, src/modules/gmail/__tests__/forward.test.ts, src/modules/gmail/__tests__/attachments.test.ts, src/modules/gmail/__tests__/manage.test.ts
Runtime rate-limiter scope test updated to expect 118 wrap calls (59 per runtime) accounting for 12 new Gmail operations. Four new comprehensive test suites validating reply/forward logic (threading headers, subject prefixing, body quoting, recipient deduplication), attachment operations (metadata extraction, download, multipart encoding), and message management (trash, delete, label modifications).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Runtime
    participant Gmail API
    participant Message Context
    participant Cache

    Client->>Runtime: replyToMessage(messageId, body)
    Runtime->>Gmail API: get(userId:'me', id:messageId)
    Gmail API-->>Runtime: original message with headers
    Runtime->>Message Context: extract threading headers<br/>(Message-ID, References)
    Message Context-->>Runtime: inReplyTo, references, threadId
    Runtime->>Runtime: build email with<br/>In-Reply-To & References
    Runtime->>Gmail API: send(raw message)
    Gmail API-->>Runtime: messageId, threadId, labelIds
    Runtime->>Cache: clear('gmail:list')
    Cache-->>Runtime: cleared
    Runtime-->>Client: { messageId, threadId, labelIds, message }
Loading
sequenceDiagram
    participant Client
    participant Runtime
    participant Validator
    participant MIME Builder
    participant Gmail API
    participant Cache

    Client->>Runtime: sendWithAttachments(to, subject, attachments)
    Runtime->>Validator: validate recipients & attachments
    Validator-->>Runtime: validated options
    Runtime->>MIME Builder: buildMultipartMessage(headers, body, attachments)
    MIME Builder->>MIME Builder: encode attachments to base64
    MIME Builder->>MIME Builder: construct multipart/mixed<br/>with boundary
    MIME Builder-->>Runtime: RFC 2822 message
    Runtime->>Runtime: encodeToBase64Url(message)
    Runtime->>Gmail API: send(raw:encoded_message)
    Gmail API-->>Runtime: messageId, threadId, labelIds
    Runtime->>Cache: clear('gmail:list')<br/>clear('gmail:search')
    Cache-->>Runtime: cleared
    Runtime-->>Client: { messageId, threadId, labelIds, message }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 A Rabbit's ode to Gmail expansion
Through threading headers, replies now flow,
Attachments dance in MIME's grand show,
Forward and manage, with flair we go,
Twelve operations strong—watch Gmail grow! 📧✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main feature addition: reply, forward, attachment, and message management operations to the Gmail module.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/gdrive-12-gmail-operations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
gdrive-mcp 64fae91 Commit Preview URL

Branch Preview URL
Mar 04 2026, 01:45 AM

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 4, 2026

📊 Type Coverage Report

Type Coverage: 98.59%

This PR's TypeScript type coverage analysis is complete.
Check the full report in the workflow artifacts.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 4, 2026

🔒 Security Scan Summary

Generated on: Wed Mar 4 01:46:29 UTC 2026
Commit: 2003f74

Scan Results

  • SAST Analysis: success
  • Dependency Scan: success
  • Secret Scan: success
  • Docker Security Scan: success
  • License Scan: success

Summary

  • Total scans: 5
  • Critical issues: 0
  • Overall status: ✅ PASS

Recommendations

  1. Review all failed scans and address critical issues
  2. Update dependencies with known vulnerabilities
  3. Ensure no secrets are committed to the repository
  4. Follow Docker security best practices
  5. Review license compliance for all dependencies

Security report generated by Claude Code

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64fae91097

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

*/
function parseEmailList(value: string | undefined): string[] {
if (!value || value.trim() === '') {return [];}
return value.split(',').map(e => e.trim()).filter(e => e.length > 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse recipient lists with RFC-aware logic

replyAllToMessage builds recipients by splitting header values on commas, but valid RFC 5322 addresses can contain commas inside quoted display names (for example "Doe, John" <john@example.com>). In those cases this split produces broken fragments that later fail recipient validation, so reply-all can fail on legitimate messages. Use a real address parser (or Gmail-parsed addresses) instead of naive comma splitting.

Useful? React with 👍 / 👎.

lines.push('Content-Transfer-Encoding: base64');
lines.push('');
// Break base64 data into 76-char lines per RFC 2045
const b64 = attachment.data.replace(/[^A-Za-z0-9+/=]/g, '');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize base64url attachment data before MIME encoding

buildMultipartMessage removes any attachment characters outside [A-Za-z0-9+/=], which drops - and _ from base64url payloads instead of converting them. Since downloadAttachment returns Gmail data in base64url form, piping downloaded content into sendWithAttachments can silently corrupt files. Convert base64url to standard base64 (-+, _/) rather than stripping those characters.

Useful? React with 👍 / 👎.

Comment on lines +249 to +251
lines.push('Content-Transfer-Encoding: quoted-printable');
lines.push('');
lines.push(body);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Encode multipart body to match declared transfer encoding

The text part is marked as Content-Transfer-Encoding: quoted-printable, but the code writes the raw body string without quoted-printable encoding. For bodies containing non-ASCII bytes or quoted-printable metacharacters (especially =), some mail clients will decode the content incorrectly and display garbled text. Either encode the body as quoted-printable or declare a transfer encoding that matches the raw content.

Useful? React with 👍 / 👎.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 4, 2026

Performance Comparison Report

Operation Performance

Operation Baseline Avg Current Avg Change Status
listFiles 95.0ms 56.6ms -40.4% 🚀 IMPROVEMENT
readFile 180.0ms 93.8ms -47.9% 🚀 IMPROVEMENT
createFile 250.0ms 146.3ms -41.5% 🚀 IMPROVEMENT
cacheOperation 45.0ms 60.6ms 34.6% ❌ REGRESSION

Memory Usage

  • Baseline: 45.2 MB
  • Current: 4.41 MB
  • Change: -90.2%

Summary

  • 🚀 Improvements: 3
  • ❌ Regressions: 1

⚠️ Performance regressions detected! Please review the changes.


Performance report generated by Claude Code

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/modules/gmail/attachments.ts (2)

210-213: Validation results are discarded.

validateAndSanitizeRecipients returns sanitized email addresses, but the return values are not used. While buildMultipartMessage will sanitize again internally, this results in redundant sanitization. Consider either using the sanitized values or switching to a validation-only function.

Option: Use sanitized values directly
   // Validate recipients (throws if any invalid)
-  validateAndSanitizeRecipients(to, 'to');
-  if (cc && cc.length > 0) {validateAndSanitizeRecipients(cc, 'cc');}
-  if (bcc && bcc.length > 0) {validateAndSanitizeRecipients(bcc, 'bcc');}
+  const sanitizedTo = validateAndSanitizeRecipients(to, 'to');
+  const sanitizedCc = cc && cc.length > 0 ? validateAndSanitizeRecipients(cc, 'cc') : undefined;
+  const sanitizedBcc = bcc && bcc.length > 0 ? validateAndSanitizeRecipients(bcc, 'bcc') : undefined;

   // Build params without passing undefined to optional fields (exactOptionalPropertyTypes)
   const messageParams: Parameters<typeof buildMultipartMessage>[0] = {
-    to,
+    to: sanitizedTo,
     subject,
     body,
     attachments: attachments || [],
   };
-  if (cc && cc.length > 0) {messageParams.cc = cc;}
-  if (bcc && bcc.length > 0) {messageParams.bcc = bcc;}
+  if (sanitizedCc) {messageParams.cc = sanitizedCc;}
+  if (sanitizedBcc) {messageParams.bcc = sanitizedBcc;}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/gmail/attachments.ts` around lines 210 - 213,
validateAndSanitizeRecipients returns sanitized addresses but its results are
ignored, causing redundant sanitization in buildMultipartMessage; update the
call sites so you capture and use the returned sanitized arrays (e.g., to =
validateAndSanitizeRecipients(to, 'to'); and likewise for cc and bcc) or replace
the calls with a validation-only helper if you intentionally want no
mutation—adjust references to to, cc, bcc before passing them into
buildMultipartMessage so the sanitized values are used.

154-161: Consider throwing an error when attachment metadata is not found.

If attachmentId doesn't match any attachment in the message, attachmentMeta will be undefined and the download proceeds with the API call succeeding but returning empty filename/mimeType. This could lead to confusing results for the caller.

Proposed fix to fail fast
   const attachmentMeta = allAttachments.find(a => a.attachmentId === attachmentId);
+  if (!attachmentMeta) {
+    throw new Error(`Attachment ${attachmentId} not found in message ${messageId}`);
+  }

   // Download the attachment content
   const response = await context.gmail.users.messages.attachments.get({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/gmail/attachments.ts` around lines 154 - 161, If attachmentMeta
(from allAttachments.find(...)) is undefined, fail fast by throwing a
descriptive error before calling context.gmail.users.messages.attachments.get;
check the result of the find for the given attachmentId (and include
messageId/attachmentId in the message) so callers get a clear failure instead of
proceeding to download with empty filename/mimeType.
src/modules/gmail/forward.ts (1)

34-40: Redundant conditional branch.

Lines 36-38 check if mimeType is text/plain or text/html and decode, but line 39 decodes unconditionally anyway when the condition is false. The entire if-else can be simplified.

Simplified version
   // Simple single-part message
   if (payload.body?.data) {
-    const mimeType = payload.mimeType || '';
-    if (mimeType === 'text/plain' || mimeType === 'text/html') {
-      return decode(payload.body.data);
-    }
     return decode(payload.body.data);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/gmail/forward.ts` around lines 34 - 40, The conditional that
checks mimeType in src/modules/gmail/forward.ts is redundant: inside the block
where payload.body?.data exists the code decodes for both the if and the else
path. Update the code inside the function handling payload to remove the
unnecessary mimeType branch and simply return decode(payload.body.data) when
payload.body?.data is present (remove the unused mimeType variable and the if
block around it); keep the surrounding null/undefined checks intact so other
behavior is unchanged.
src/modules/gmail/types.ts (1)

551-559: Use a literal true type for safetyAcknowledged to enforce the safety contract at compile time.

Currently at line 558, boolean allows false values to pass type checking. While runtime validation at line 129 of manage.ts rejects falsy values with an error, using a literal true type would catch this mistake during development rather than at runtime. For an irreversible operation, encoding the requirement in the type system prevents unsafe calls from being written in the first place.

Suggested fix
 export interface DeleteMessageOptions {
   /** The message ID */
   id: string;
   /**
    * Must be true to confirm permanent deletion.
    * This operation cannot be undone — use trashMessage() for recoverable deletion.
    */
-  safetyAcknowledged: boolean;
+  safetyAcknowledged: true;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/gmail/types.ts` around lines 551 - 559, Change the
DeleteMessageOptions interface to require the literal true type for
safetyAcknowledged (replace boolean with the literal true) so callers must pass
true at compile time; update any call sites that construct DeleteMessageOptions
(e.g., where delete message requests are created) to pass the literal true value
and adjust any tests or helpers that currently pass false or a boolean variable;
leave runtime validation in manage.ts unchanged but rely on the type-level
enforcement provided by the modified DeleteMessageOptions.safetyAcknowledged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/gmail/manage.ts`:
- Around line 52-54: The cache invalidation after message mutations currently
only clears per-message and list keys (calls to context.cacheManager.invalidate
like invalidate(`gmail:getMessage:${id}`) and invalidate('gmail:list')); add an
additional invalidate('gmail:search') call in the same mutation code paths
(trash/untrash/delete/read-state/archive handlers) so search results are cleared
too; update each block where those two invalidates exist (the occurrences around
the existing invalidate calls and the other instances noted) to call
context.cacheManager.invalidate('gmail:search') immediately alongside the other
invalidates.

In `@src/modules/gmail/reply.ts`:
- Around line 77-113: The code currently selects the reply recipient from the
original "From" header only; update the header extraction and recipient
selection to prefer "Reply-To" and fall back to "From" (use findHeader(headers,
'Reply-To') then findHeader(headers, 'From')), update the metadataHeaders array
to include 'Reply-To', and set messageParams.to to use the chosen replyRecipient
(e.g., replyRecipient variable) instead of originalFrom; additionally add a
guard that throws or returns a clear error if neither Reply-To nor From is
present to avoid sending with an invalid recipient (refer to
originalMessageId/originalReferences handling, buildEmailMessage, and
replySubject to keep threading and subject logic unchanged).
- Around line 26-28: parseEmailList currently splits on commas which breaks
quoted display names (e.g. "Doe, Jane" <jane@example.com>) and can corrupt
dedupe/self-exclusion and recipient lists; update parseEmailList to parse
RFC-5322 style address headers instead of simple split: either use a
CSV/quoted-string aware split or implement/extract addresses by matching address
tokens (look for angle-bracketed addresses like <...> and fallback to bare
addresses) so you return only canonical email addresses for
dedupe/self-exclusion logic; locate and change the parseEmailList function and
ensure callers that rely on its output (dedupe/self-exclusion) receive correctly
parsed addresses.

In `@src/modules/gmail/utils.ts`:
- Around line 248-252: The code writes the body verbatim but sets
Content-Transfer-Encoding: quoted-printable; either actually quoted-printable
encode the body or change the header to a truthful encoding. Fix by updating the
block that uses isHtml/body/lines (the lines.push('Content-Transfer-Encoding:
...') call) to either 1) replace that header with 'Content-Transfer-Encoding:
8bit' when you will send raw UTF-8 body, or 2) import/use a quoted-printable
encoder and run body = quotedPrintableEncode(body) before pushing the body and
keep the header as quoted-printable; ensure the chosen approach is applied for
both HTML and plain branches so the header matches the body content.

---

Nitpick comments:
In `@src/modules/gmail/attachments.ts`:
- Around line 210-213: validateAndSanitizeRecipients returns sanitized addresses
but its results are ignored, causing redundant sanitization in
buildMultipartMessage; update the call sites so you capture and use the returned
sanitized arrays (e.g., to = validateAndSanitizeRecipients(to, 'to'); and
likewise for cc and bcc) or replace the calls with a validation-only helper if
you intentionally want no mutation—adjust references to to, cc, bcc before
passing them into buildMultipartMessage so the sanitized values are used.
- Around line 154-161: If attachmentMeta (from allAttachments.find(...)) is
undefined, fail fast by throwing a descriptive error before calling
context.gmail.users.messages.attachments.get; check the result of the find for
the given attachmentId (and include messageId/attachmentId in the message) so
callers get a clear failure instead of proceeding to download with empty
filename/mimeType.

In `@src/modules/gmail/forward.ts`:
- Around line 34-40: The conditional that checks mimeType in
src/modules/gmail/forward.ts is redundant: inside the block where
payload.body?.data exists the code decodes for both the if and the else path.
Update the code inside the function handling payload to remove the unnecessary
mimeType branch and simply return decode(payload.body.data) when
payload.body?.data is present (remove the unused mimeType variable and the if
block around it); keep the surrounding null/undefined checks intact so other
behavior is unchanged.

In `@src/modules/gmail/types.ts`:
- Around line 551-559: Change the DeleteMessageOptions interface to require the
literal true type for safetyAcknowledged (replace boolean with the literal true)
so callers must pass true at compile time; update any call sites that construct
DeleteMessageOptions (e.g., where delete message requests are created) to pass
the literal true value and adjust any tests or helpers that currently pass false
or a boolean variable; leave runtime validation in manage.ts unchanged but rely
on the type-level enforcement provided by the modified
DeleteMessageOptions.safetyAcknowledged.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1bb7392 and 64fae91.

📒 Files selected for processing (15)
  • src/__tests__/sdk/runtime-rate-limiter-scope.test.ts
  • src/modules/gmail/__tests__/attachments.test.ts
  • src/modules/gmail/__tests__/forward.test.ts
  • src/modules/gmail/__tests__/manage.test.ts
  • src/modules/gmail/__tests__/reply.test.ts
  • src/modules/gmail/attachments.ts
  • src/modules/gmail/forward.ts
  • src/modules/gmail/index.ts
  • src/modules/gmail/manage.ts
  • src/modules/gmail/reply.ts
  • src/modules/gmail/types.ts
  • src/modules/gmail/utils.ts
  • src/sdk/runtime.ts
  • src/sdk/spec.ts
  • src/sdk/types.ts

Comment on lines +52 to +54
await context.cacheManager.invalidate(`gmail:getMessage:${id}`);
await context.cacheManager.invalidate('gmail:list');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Invalidate gmail:search after message mutations to prevent stale reads.

At Line 52/53 and the equivalent blocks in other operations, only per-message/list caches are invalidated. Search results can stay stale after trash/untrash/delete/read-state/archive updates.

Suggested fix
@@
   await context.cacheManager.invalidate(`gmail:getMessage:${id}`);
   await context.cacheManager.invalidate('gmail:list');
+  await context.cacheManager.invalidate('gmail:search');
@@
   await context.cacheManager.invalidate(`gmail:getMessage:${id}`);
   await context.cacheManager.invalidate('gmail:list');
+  await context.cacheManager.invalidate('gmail:search');
@@
   await context.cacheManager.invalidate(`gmail:getMessage:${id}`);
   await context.cacheManager.invalidate('gmail:list');
+  await context.cacheManager.invalidate('gmail:search');
@@
   await context.cacheManager.invalidate(`gmail:getMessage:${id}`);
   await context.cacheManager.invalidate('gmail:list');
+  await context.cacheManager.invalidate('gmail:search');
@@
   await context.cacheManager.invalidate(`gmail:getMessage:${id}`);
   await context.cacheManager.invalidate('gmail:list');
+  await context.cacheManager.invalidate('gmail:search');
@@
   await context.cacheManager.invalidate(`gmail:getMessage:${id}`);
   await context.cacheManager.invalidate('gmail:list');
+  await context.cacheManager.invalidate('gmail:search');

Also applies to: 91-93, 142-144, 184-186, 227-229, 272-274

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/gmail/manage.ts` around lines 52 - 54, The cache invalidation
after message mutations currently only clears per-message and list keys (calls
to context.cacheManager.invalidate like invalidate(`gmail:getMessage:${id}`) and
invalidate('gmail:list')); add an additional invalidate('gmail:search') call in
the same mutation code paths (trash/untrash/delete/read-state/archive handlers)
so search results are cleared too; update each block where those two invalidates
exist (the occurrences around the existing invalidate calls and the other
instances noted) to call context.cacheManager.invalidate('gmail:search')
immediately alongside the other invalidates.

Comment on lines +26 to +28
function parseEmailList(value: string | undefined): string[] {
if (!value || value.trim() === '') {return [];}
return value.split(',').map(e => e.trim()).filter(e => e.length > 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

parseEmailList breaks valid quoted address headers containing commas.

Line 28 uses split(','), which mis-parses values like "Doe, Jane" <jane@example.com>, team@example.com. That can corrupt dedupe/self-exclusion and send to malformed recipients.

Suggested fix
 function parseEmailList(value: string | undefined): string[] {
   if (!value || value.trim() === '') {return [];}
-  return value.split(',').map(e => e.trim()).filter(e => e.length > 0);
+  const result: string[] = [];
+  let current = '';
+  let inQuotes = false;
+  let angleDepth = 0;
+
+  for (const ch of value) {
+    if (ch === '"' && angleDepth === 0) {inQuotes = !inQuotes;}
+    if (!inQuotes) {
+      if (ch === '<') {angleDepth += 1;}
+      if (ch === '>') {angleDepth = Math.max(0, angleDepth - 1);}
+      if (ch === ',' && angleDepth === 0) {
+        const token = current.trim();
+        if (token) {result.push(token);}
+        current = '';
+        continue;
+      }
+    }
+    current += ch;
+  }
+
+  const token = current.trim();
+  if (token) {result.push(token);}
+  return result;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/gmail/reply.ts` around lines 26 - 28, parseEmailList currently
splits on commas which breaks quoted display names (e.g. "Doe, Jane"
<jane@example.com>) and can corrupt dedupe/self-exclusion and recipient lists;
update parseEmailList to parse RFC-5322 style address headers instead of simple
split: either use a CSV/quoted-string aware split or implement/extract addresses
by matching address tokens (look for angle-bracketed addresses like <...> and
fallback to bare addresses) so you return only canonical email addresses for
dedupe/self-exclusion logic; locate and change the parseEmailList function and
ensure callers that rely on its output (dedupe/self-exclusion) receive correctly
parsed addresses.

Comment on lines +77 to +113
const originalResponse = await context.gmail.users.messages.get({
userId: 'me',
id: messageId,
format: 'metadata',
metadataHeaders: ['From', 'Subject', 'Message-ID', 'References', 'To'],
});

const originalData = originalResponse.data;
const headers = originalData.payload?.headers || [];

const originalFrom = findHeader(headers, 'From');
const originalSubject = findHeader(headers, 'Subject');
const originalMessageId = findHeader(headers, 'Message-ID');
const originalReferences = findHeader(headers, 'References');
const threadId = originalData.threadId || '';

// Build threading headers from the original message's MIME Message-ID
let inReplyTo: string | undefined;
let references: string | undefined;

if (originalMessageId) {
inReplyTo = originalMessageId;
// References = original References + original Message-ID
references = originalReferences
? `${originalReferences} ${originalMessageId}`
: originalMessageId;
}

// Determine subject
const subject = replySubject(originalSubject);

// Build params without passing undefined to optional fields (exactOptionalPropertyTypes)
const messageParams: Parameters<typeof buildEmailMessage>[0] = {
to: [originalFrom],
subject,
body,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use Reply-To (fallback From) for reply recipient selection.

At Line 81 and Line 197, metadataHeaders omits Reply-To. At Line 110 and Line 224, replies are addressed from From only. For list/automated mail, this can route replies to the wrong mailbox. Also add a guard when neither header is usable so we fail with a clear error instead of sending an invalid recipient.

Suggested fix
@@
-    metadataHeaders: ['From', 'Subject', 'Message-ID', 'References', 'To'],
+    metadataHeaders: ['Reply-To', 'From', 'Subject', 'Message-ID', 'References', 'To'],
@@
+  const originalReplyTo = findHeader(headers, 'Reply-To');
   const originalFrom = findHeader(headers, 'From');
@@
+  const replyTarget = (originalReplyTo || originalFrom).trim();
+  if (!replyTarget) {
+    throw new Error('Cannot determine reply recipient: missing Reply-To/From header');
+  }
@@
-    to: [originalFrom],
+    to: [replyTarget],
@@
-    metadataHeaders: ['From', 'To', 'Cc', 'Subject', 'Message-ID', 'References'],
+    metadataHeaders: ['Reply-To', 'From', 'To', 'Cc', 'Subject', 'Message-ID', 'References'],
@@
+  const originalReplyTo = findHeader(headers, 'Reply-To');
   const originalFrom = findHeader(headers, 'From');
@@
-  toAddresses.push(originalFrom);
+  toAddresses.push((originalReplyTo || originalFrom).trim());
@@
-    const bare = extractEmailAddress(addr).toLowerCase();
+    const bare = extractEmailAddress(addr).toLowerCase();
+    if (!bare) {continue;}

Also applies to: 193-226

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/gmail/reply.ts` around lines 77 - 113, The code currently selects
the reply recipient from the original "From" header only; update the header
extraction and recipient selection to prefer "Reply-To" and fall back to "From"
(use findHeader(headers, 'Reply-To') then findHeader(headers, 'From')), update
the metadataHeaders array to include 'Reply-To', and set messageParams.to to use
the chosen replyRecipient (e.g., replyRecipient variable) instead of
originalFrom; additionally add a guard that throws or returns a clear error if
neither Reply-To nor From is present to avoid sending with an invalid recipient
(refer to originalMessageId/originalReferences handling, buildEmailMessage, and
replySubject to keep threading and subject logic unchanged).

Comment on lines +248 to +252
lines.push(`Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset="UTF-8"`);
lines.push('Content-Transfer-Encoding: quoted-printable');
lines.push('');
lines.push(body);
lines.push('');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Body part declares quoted-printable encoding but content is not encoded.

Line 249 sets Content-Transfer-Encoding: quoted-printable but the body is written verbatim on line 251. If the body contains characters that require quoted-printable encoding (e.g., lines >76 chars, non-ASCII, = signs), the message may be malformed. Either apply proper quoted-printable encoding or use Content-Transfer-Encoding: 8bit (or 7bit for ASCII-only).

Proposed fix using 8bit encoding (simpler)
   lines.push(`--${boundary}`);
   lines.push(`Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset="UTF-8"`);
-  lines.push('Content-Transfer-Encoding: quoted-printable');
+  lines.push('Content-Transfer-Encoding: 8bit');
   lines.push('');
   lines.push(body);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lines.push(`Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset="UTF-8"`);
lines.push('Content-Transfer-Encoding: quoted-printable');
lines.push('');
lines.push(body);
lines.push('');
lines.push(`Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset="UTF-8"`);
lines.push('Content-Transfer-Encoding: 8bit');
lines.push('');
lines.push(body);
lines.push('');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/gmail/utils.ts` around lines 248 - 252, The code writes the body
verbatim but sets Content-Transfer-Encoding: quoted-printable; either actually
quoted-printable encode the body or change the header to a truthful encoding.
Fix by updating the block that uses isHtml/body/lines (the
lines.push('Content-Transfer-Encoding: ...') call) to either 1) replace that
header with 'Content-Transfer-Encoding: 8bit' when you will send raw UTF-8 body,
or 2) import/use a quoted-printable encoder and run body =
quotedPrintableEncode(body) before pushing the body and keep the header as
quoted-printable; ensure the chosen approach is applied for both HTML and plain
branches so the header matches the body content.

@AojdevStudio AojdevStudio merged commit 2e01895 into main Mar 4, 2026
36 checks passed
@AojdevStudio AojdevStudio deleted the feature/gdrive-12-gmail-operations branch March 4, 2026 01:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant