Skip to content

feat(gmail): add listDrafts, getDraft, updateDraft, deleteDraft operations#62

Merged
AojdevStudio merged 2 commits intomainfrom
feature/gdrive-11-gmail-draft-management
Mar 28, 2026
Merged

feat(gmail): add listDrafts, getDraft, updateDraft, deleteDraft operations#62
AojdevStudio merged 2 commits intomainfrom
feature/gdrive-11-gmail-draft-management

Conversation

@AojdevStudio
Copy link
Copy Markdown
Owner

@AojdevStudio AojdevStudio commented Mar 28, 2026

Summary

Implements Gmail draft management (GDRIVE-11). We had createDraft and sendDraft but no way to list, read, update, or delete drafts.

Changes

File Change
src/modules/gmail/drafts.ts New module with 4 draft operations
src/modules/gmail/types.ts 8 new types (options + results for each op)
src/modules/gmail/index.ts Exports new functions and types
src/sdk/runtime.ts 4 new rate-limited SDK wrappers
src/sdk/types.ts 4 new SDKRuntime gmail method signatures
src/sdk/spec.ts Full spec entries for all 4 operations
src/tools/listTools.ts Discovery entries for all 4 operations
src/__tests__/gmail/drafts.test.ts 14 unit tests
src/__tests__/sdk/runtime-rate-limiter-scope.test.ts Updated wrap count (65→69)

Operations

  • listDrafts — paginated list with draft ID, subject, recipient, snippet
  • getDraft — full content: all headers + decoded body (plain text and HTML)
  • updateDraft — modifies existing draft in place; draft ID preserved
  • deleteDraft — permanently removes draft by ID

Acceptance Criteria

  • listDrafts returns draft IDs, subjects, and snippets
  • getDraft returns full draft content (to, subject, body, cc, bcc)
  • updateDraft modifies an existing draft without creating a new one
  • deleteDraft removes a draft by ID
  • All four operations registered in Gmail service spec

Testing

All 51 test suites pass (585 tests), type check clean.

Closes #42
Resolves GDRIVE-11

Summary by CodeRabbit

  • New Features

    • Added Gmail draft management: list drafts with pagination, retrieve full draft details including headers and body, update draft recipients and message content, and permanently delete drafts.
  • Tests

    • Added comprehensive test coverage for all draft operations and updated rate limiter expectations.

…tions

Implements Gmail draft management (GDRIVE-11):
- listDrafts: paginated list with subject, recipient, and snippet
- getDraft: full draft content with decoded body (plain and HTML)
- updateDraft: modify existing draft in place (preserves draft ID)
- deleteDraft: permanently remove a draft by ID

Changes:
- src/modules/gmail/drafts.ts — new module with 4 draft operations
- src/modules/gmail/types.ts — 8 new types (options + results)
- src/modules/gmail/index.ts — exports new functions and types
- src/sdk/runtime.ts — 4 new rate-limited SDK wrappers
- src/sdk/types.ts — 4 new SDKRuntime gmail methods
- src/sdk/spec.ts — spec entries for all 4 operations
- src/tools/listTools.ts — discovery entries for all 4 operations
- src/__tests__/gmail/drafts.test.ts — 14 unit tests
- src/__tests__/sdk/runtime-rate-limiter-scope.test.ts — updated wrap count
@linear
Copy link
Copy Markdown

linear Bot commented Mar 28, 2026

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 28, 2026

Warning

Rate limit exceeded

@AojdevStudio has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 36 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 14 minutes and 36 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 478828a3-f29d-45a5-9271-9be79db83b3a

📥 Commits

Reviewing files that changed from the base of the PR and between 5fb7804 and dd69a64.

📒 Files selected for processing (1)
  • src/modules/gmail/drafts.ts
📝 Walkthrough

Walkthrough

This pull request adds complete Gmail draft management functionality with four new operations: listDrafts, getDraft, updateDraft, and deleteDraft. The implementation includes type definitions, SDK runtime integration, test coverage, and tool discovery updates, enabling users to list, retrieve, modify, and delete Gmail drafts programmatically.

Changes

Cohort / File(s) Summary
Draft Module Implementation
src/modules/gmail/drafts.ts
New module exporting four draft operations with pagination support, multipart email body parsing, header extraction, base64 encoding/decoding, cache invalidation, and performance tracking integration.
Type Definitions
src/modules/gmail/types.ts
Added 9 new exported interfaces for draft operations including options and result types for list, get, update, and delete operations with support for pagination, full email headers, and body content.
Module & SDK Integration
src/modules/gmail/index.ts, src/sdk/runtime.ts, src/sdk/types.ts, src/sdk/spec.ts
Extended Gmail module exports, added draft operations to SDK runtime with rate-limiter wrapping, updated SDK type definitions and specification with operation signatures and documentation.
Tool Discovery
src/tools/listTools.ts
Added four new draft operations to the Gmail module's public tool structure with signatures, descriptions, and usage examples.
Comprehensive Test Suite
src/__tests__/gmail/drafts.test.ts
New test file with extensive coverage for all four draft operations including pagination, edge cases, error handling, cache invalidation verification, and multipart email parsing validation.
Rate Limiter Test Update
src/__tests__/sdk/runtime-rate-limiter-scope.test.ts
Updated rate limiter wrapper invocation count expectations to reflect the addition of four new wrapped draft operations (from 130 to 138 total invocations).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

Feature, enhancement

Poem

🐰 Drafts now dance in Gmail's domain,
List them, read them, update their refrain,
Delete the old words, keep what's divine,
Four new operations—perfectly aligned! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main change: adding four new Gmail draft operations (listDrafts, getDraft, updateDraft, deleteDraft), which aligns with the changeset's core purpose.
Linked Issues check ✅ Passed All five acceptance criteria from issue #42 are met: listDrafts returns draft IDs/subjects/snippets [types.ts, drafts.ts], getDraft returns full draft content including headers/body [types.ts, drafts.ts], updateDraft modifies drafts in place [drafts.ts, updateDraft function], deleteDraft removes drafts [drafts.ts, deleteDraft function], and all four operations are registered in SDK spec [spec.ts, runtime.ts, types.ts].
Out of Scope Changes check ✅ Passed All changes directly support the PR objectives: new draft operations implementation (drafts.ts, types.ts), SDK/runtime integration (runtime.ts, types.ts, spec.ts), module exports (index.ts), tool discovery (listTools.ts), and test coverage (drafts.test.ts, runtime-rate-limiter-scope.test.ts). No extraneous modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% 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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/gdrive-11-gmail-draft-management

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.

@github-actions
Copy link
Copy Markdown

📊 Type Coverage Report

Type Coverage: 98.63%

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

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

cloudflare-workers-and-pages Bot commented Mar 28, 2026

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 dd69a64 Commit Preview URL

Branch Preview URL
Mar 28 2026, 08:49 PM

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 28, 2026

🔒 Security Scan Summary

Generated on: Sat Mar 28 20:50:15 UTC 2026
Commit: cea3792

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: 5fb7804c2a

ℹ️ 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".

options: ListDraftsOptions,
context: GmailContext
): Promise<ListDraftsResult> {
const { maxResults = 10, pageToken } = options;
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 Default listDrafts options when args are omitted

listDrafts destructures options immediately, but the SDK spec added in this commit documents listDrafts(options?) and code-mode callers can invoke sdk.gmail.listDrafts() with no argument. In that case options is undefined and this throws before reaching Gmail (Cannot destructure property 'maxResults' ...). Please default the parameter (or wrapper input) to {} so the zero-arg call path works as documented.

Useful? React with 👍 / 👎.

Comment on lines +161 to +164
} else if (payload.parts) {
// Prefer plain text, fall back to HTML
const textPart = payload.parts.find((p) => p.mimeType === 'text/plain');
const htmlPart = payload.parts.find((p) => p.mimeType === 'text/html');
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 Traverse nested MIME parts when reading draft body

This branch only checks top-level payload.parts for text/plain/text/html. Drafts that include attachments are commonly multipart/mixed with the text body nested inside multipart/alternative, so getDraft returns an empty body even though content exists. Recursively scanning nested parts (similar to read.ts message parsing) is needed to reliably return full draft content.

Useful? React with 👍 / 👎.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 28, 2026

Performance Comparison Report

Operation Performance

Operation Baseline Avg Current Avg Change Status
listFiles 95.0ms 44.9ms -52.7% 🚀 IMPROVEMENT
readFile 180.0ms 99.0ms -45.0% 🚀 IMPROVEMENT
createFile 250.0ms 144.2ms -42.3% 🚀 IMPROVEMENT
cacheOperation 45.0ms 50.8ms 12.9% ❌ 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: 5

🧹 Nitpick comments (1)
src/modules/gmail/types.ts (1)

653-657: Minor: Redundant | undefined with optional property.

Line 655 uses nextPageToken?: string | undefined;. The ? already makes the property optional (allowing undefined), so | undefined is redundant.

✨ Suggested simplification
 export interface ListDraftsResult {
   drafts: DraftSummary[];
-  nextPageToken?: string | undefined;
+  nextPageToken?: string;
   resultSizeEstimate: number;
 }
🤖 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 653 - 657, The ListDraftsResult
interface has a redundant `| undefined` on the optional property nextPageToken
(declaration: nextPageToken?: string | undefined); remove the `| undefined` so
the property is declared simply as nextPageToken?: string to keep the type
concise and rely on the `?` to allow undefined—update the ListDraftsResult
interface accordingly.
🤖 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/__tests__/gmail/drafts.test.ts`:
- Around line 183-186: The Gmail draft decoding uses standard base64 instead of
base64url; in src/modules/gmail/drafts.ts update the Buffer.from(..., 'base64')
calls in the getDraft implementation (the places noted around payload.body.data
usage) to use 'base64url' so message bodies decode correctly. Also update the
test fixture in src/__tests__/gmail/drafts.test.ts that currently uses
Buffer.from('Hello world').toString('base64') to use 'base64url' so the test
reflects the real API encoding and doesn't mask the bug.

In `@src/modules/gmail/drafts.ts`:
- Around line 57-95: The current Promise.all over drafts (draftSummaries created
from drafts.map calling context.gmail.users.drafts.get) can fire hundreds of
concurrent requests; limit concurrency by replacing the direct
Promise.all(drafts.map(...)) pattern with a bounded concurrency approach (e.g.,
use p-limit with a CONCURRENCY constant or implement a small promise
pool/batching loop) and run at most N parallel calls to
context.gmail.users.drafts.get (suggest N=5–20); preserve the same per-draft
error handling and returned object shape (draftId, messageId, subject, to,
snippet) inside the worker function used by the limiter so behavior is unchanged
except for throttling.
- Line 176: The code logs sensitive user content by calling context.logger.info
with the draft subject; remove the subject from info-level logs and log only
non-sensitive identifiers (e.g., draftId) instead. Locate the
context.logger.info calls in src/modules/gmail/drafts.ts (the "Retrieved draft"
log and the similar occurrence later) and change them to log only draftId (or
masked/hashed identifiers) at info level; if you still need the subject for
troubleshooting, send it to a debug-level log (context.logger.debug) or store it
in a secure/audited location rather than info.
- Around line 223-228: The consecutive single-line if statements assigning to
msgOptions (e.g., the checks for cc, bcc, isHtml, from, inReplyTo, references)
violate the enforced curly rule; update each conditional (if (cc), if (bcc), if
(isHtml !== undefined), if (from), if (inReplyTo), if (references)) to use
braces around the assignment statement (e.g., if (cc) { msgOptions.cc = cc; })
so every conditional block in drafts.ts uses explicit braces.
- Around line 151-173: The MIME parsing only checks payload and first-level
payload.parts so nested multipart structures are missed; update the logic in the
drafts parsing function (where payload, payload.parts, body and isHtml are
handled) to perform a recursive traversal of parts: write a helper (e.g.,
findPartRecursive or getBodyFromParts) that walks parts at any depth, looks for
a text/plain part first and returns its decoded body, otherwise falls back to
text/html and sets isHtml=true; use that helper to set body/isHtml instead of
directly accessing payload.parts, and keep existing base64 decoding behavior for
part.body.data.

---

Nitpick comments:
In `@src/modules/gmail/types.ts`:
- Around line 653-657: The ListDraftsResult interface has a redundant `|
undefined` on the optional property nextPageToken (declaration: nextPageToken?:
string | undefined); remove the `| undefined` so the property is declared simply
as nextPageToken?: string to keep the type concise and rely on the `?` to allow
undefined—update the ListDraftsResult interface accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dc9e065a-be5d-4104-91fa-9878189d7c51

📥 Commits

Reviewing files that changed from the base of the PR and between 4d47a08 and 5fb7804.

📒 Files selected for processing (9)
  • src/__tests__/gmail/drafts.test.ts
  • src/__tests__/sdk/runtime-rate-limiter-scope.test.ts
  • src/modules/gmail/drafts.ts
  • src/modules/gmail/index.ts
  • src/modules/gmail/types.ts
  • src/sdk/runtime.ts
  • src/sdk/spec.ts
  • src/sdk/types.ts
  • src/tools/listTools.ts

Comment on lines +183 to +186
body: {
data: Buffer.from('Hello world').toString('base64'),
},
},
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how the drafts implementation decodes body data
rg -n 'base64|Buffer\.from' src/modules/gmail/drafts.ts -C 3

Repository: AojdevStudio/gdrive

Length of output: 923


🏁 Script executed:

#!/bin/bash
# Check for base64url references in the codebase
rg -n 'base64url' src/ --type ts

# Check for Gmail API documentation comments
rg -n 'Gmail|gmail' src/modules/gmail/drafts.ts -i | head -20

# Look for any encoding handling or comments about base64
rg -n 'encoding|decode' src/modules/gmail/drafts.ts -i

Repository: AojdevStudio/gdrive

Length of output: 7090


🌐 Web query:

Gmail API message body encoding format base64url

💡 Result:

The Gmail API uses base64url encoding (also called URL-safe base64) for message body data in two main contexts: 1. The raw field of a Message (when format=RAW): Contains the entire RFC 2822 formatted email message as a base64url encoded string. 2. The data field of MessagePartBody (in payload.parts[].body.data): Contains individual MIME part body data (text/plain, text/html, attachments, etc.) as base64url encoded strings. Base64url differs from standard base64 by using '-' instead of '+', '' instead of '/', and omitting padding '=' characters. To decode, replace '-' with '+', '' with '/', add padding if needed (length % 4 == 2 adds '==', == 3 adds '='), then use standard base64 decoding followed by UTF-8 decoding for text. Example JavaScript decode: function decodeBase64Url(data) { let base64 = data.replace(/-/g, '+').replace(/_/g, '/'); while (base64.length % 4) base64 += '='; return atob(base64); } const text = decodeBase64Url(payload.parts[0].body.data); Official docs confirm this format consistently across recent versions (up to 2026).

Citations:


Fix base64url decoding in getDraft implementation.

The Gmail API returns base64url-encoded message bodies, but the implementation uses standard base64 decoding at lines 157, 159, 167, and 169 in src/modules/gmail/drafts.ts:

Buffer.from(payload.body.data, 'base64').toString('utf-8')

Should be:

Buffer.from(payload.body.data, 'base64url').toString('utf-8')

This bug is masked by the test at line 184, which uses Buffer.from('Hello world').toString('base64') instead of 'base64url'. The test string encodes identically in both formats. In production, messages containing binary data or special characters would fail to decode correctly. Other modules in the codebase (forward.ts, read.ts) correctly use base64url decoding.

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

In `@src/__tests__/gmail/drafts.test.ts` around lines 183 - 186, The Gmail draft
decoding uses standard base64 instead of base64url; in
src/modules/gmail/drafts.ts update the Buffer.from(..., 'base64') calls in the
getDraft implementation (the places noted around payload.body.data usage) to use
'base64url' so message bodies decode correctly. Also update the test fixture in
src/__tests__/gmail/drafts.test.ts that currently uses Buffer.from('Hello
world').toString('base64') to use 'base64url' so the test reflects the real API
encoding and doesn't mask the bug.

Comment on lines +57 to +95
// Fetch subject/snippet for each draft in parallel (using metadata format for efficiency)
const draftSummaries = await Promise.all(
drafts.map(async (draftRef: gmail_v1.Schema$Draft) => {
if (!draftRef.id) {
return null;
}
try {
const draftResponse = await context.gmail.users.drafts.get({
userId: 'me',
id: draftRef.id,
format: 'metadata',
});

const headers = draftResponse.data.message?.payload?.headers || [];
const subject =
headers.find((h) => h.name?.toLowerCase() === 'subject')?.value ?? '(no subject)';
const to =
headers.find((h) => h.name?.toLowerCase() === 'to')?.value ?? '';
const snippet = draftResponse.data.message?.snippet ?? '';

return {
draftId: draftRef.id,
messageId: draftResponse.data.message?.id ?? '',
subject,
to,
snippet,
};
} catch {
// If we cannot fetch the individual draft, return minimal info
return {
draftId: draftRef.id,
messageId: '',
subject: '(unavailable)',
to: '',
snippet: '',
};
}
})
);
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

Cap per-draft fetch concurrency to prevent quota spikes.

This currently launches up to 500 drafts.get requests at once. That burst pattern can trigger throttling and unstable partial output under load.

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

In `@src/modules/gmail/drafts.ts` around lines 57 - 95, The current Promise.all
over drafts (draftSummaries created from drafts.map calling
context.gmail.users.drafts.get) can fire hundreds of concurrent requests; limit
concurrency by replacing the direct Promise.all(drafts.map(...)) pattern with a
bounded concurrency approach (e.g., use p-limit with a CONCURRENCY constant or
implement a small promise pool/batching loop) and run at most N parallel calls
to context.gmail.users.drafts.get (suggest N=5–20); preserve the same per-draft
error handling and returned object shape (draftId, messageId, subject, to,
snippet) inside the worker function used by the limiter so behavior is unchanged
except for throttling.

Comment on lines +151 to +173
// Decode the message body
let body = '';
let isHtml = false;
const payload = data.message?.payload;
if (payload) {
if (payload.mimeType === 'text/plain' && payload.body?.data) {
body = Buffer.from(payload.body.data, 'base64').toString('utf-8');
} else if (payload.mimeType === 'text/html' && payload.body?.data) {
body = Buffer.from(payload.body.data, 'base64').toString('utf-8');
isHtml = true;
} else if (payload.parts) {
// Prefer plain text, fall back to HTML
const textPart = payload.parts.find((p) => p.mimeType === 'text/plain');
const htmlPart = payload.parts.find((p) => p.mimeType === 'text/html');

if (textPart?.body?.data) {
body = Buffer.from(textPart.body.data, 'base64').toString('utf-8');
} else if (htmlPart?.body?.data) {
body = Buffer.from(htmlPart.body.data, 'base64').toString('utf-8');
isHtml = true;
}
}
}
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

🧩 Analysis chain

🏁 Script executed:

# First, check the actual file to confirm the code exists
cat -n src/modules/gmail/drafts.ts | head -200

Repository: AojdevStudio/gdrive

Length of output: 7365


🏁 Script executed:

# Look for the Gmail API type definitions to understand the schema
fd -e ts src/modules/gmail | head -20

Repository: AojdevStudio/gdrive

Length of output: 436


🌐 Web query:

Gmail API v1 message payload structure nested multipart handling

💡 Result:

The Gmail API v1 Message resource contains a payload field of type MessagePart, which represents the parsed MIME structure of the email. For nested multipart handling: - The top-level payload is a MessagePart object. - MessagePart fields: - mimeType: e.g., "multipart/mixed", "multipart/alternative", "text/plain". - partId: Unique ID for the part (empty for root). - headers: Array of Header objects (name/value pairs). - body: MessagePartBody with data (base64url-encoded content), size, or attachmentId. - parts: Array of child MessagePart objects (only for container types like multipart/*; empty for leaf types like text/plain). To handle nested multipart: 1. Start with message.payload. 2. If mimeType starts with "multipart/", recurse into parts[] array. 3. Leaf parts (no parts) contain content in body.data (decode base64url). 4. Use users.messages.get with format=full to get parsed payload (default behavior). Example structure for multipart/mixed with alternative text/html: { "payload": { "mimeType": "multipart/mixed", "parts": [ { "mimeType": "multipart/alternative", "parts": [ {"partId": "0.0", "mimeType": "text/plain", "body": {"data": "base64url_text"}}, {"partId": "0.1", "mimeType": "text/html", "body": {"data": "base64url_html"}} ] }, {"partId": "1", "mimeType": "image/png", "body": {"attachmentId": "A"}} ] } } Traverse recursively by partId and mimeType to extract bodies or fetch attachments via users.messages.attachments.get using attachmentId. To decode body.data: Replace '-' with '+', '_' with '/', pad with '=', then base64 decode.

Citations:


Add recursive MIME part traversal to handle nested multipart structures.

The current parser only checks first-level payload.parts and cannot retrieve message bodies from nested multipart structures (e.g., text/plain nested within multipart/alternative inside multipart/mixed). This is a common structure in Gmail API responses. Implement recursive traversal to find text content at any nesting depth.

Proposed fix (recursive MIME part lookup)
+  const findPartByMime = (
+    part: gmail_v1.Schema$MessagePart | undefined,
+    mimeType: 'text/plain' | 'text/html'
+  ): gmail_v1.Schema$MessagePart | undefined => {
+    if (!part) return undefined;
+    if (part.mimeType === mimeType && part.body?.data) return part;
+    for (const child of part.parts ?? []) {
+      const found = findPartByMime(child, mimeType);
+      if (found) return found;
+    }
+    return undefined;
+  };
+
   // Decode the message body
   let body = '';
   let isHtml = false;
   const payload = data.message?.payload;
   if (payload) {
-    if (payload.mimeType === 'text/plain' && payload.body?.data) {
-      body = Buffer.from(payload.body.data, 'base64').toString('utf-8');
-    } else if (payload.mimeType === 'text/html' && payload.body?.data) {
-      body = Buffer.from(payload.body.data, 'base64').toString('utf-8');
-      isHtml = true;
-    } else if (payload.parts) {
-      // Prefer plain text, fall back to HTML
-      const textPart = payload.parts.find((p) => p.mimeType === 'text/plain');
-      const htmlPart = payload.parts.find((p) => p.mimeType === 'text/html');
-
-      if (textPart?.body?.data) {
-        body = Buffer.from(textPart.body.data, 'base64').toString('utf-8');
-      } else if (htmlPart?.body?.data) {
-        body = Buffer.from(htmlPart.body.data, 'base64').toString('utf-8');
-        isHtml = true;
-      }
-    }
+    const textPart = findPartByMime(payload, 'text/plain');
+    const htmlPart = findPartByMime(payload, 'text/html');
+    if (textPart?.body?.data) {
+      body = Buffer.from(textPart.body.data, 'base64').toString('utf-8');
+    } else if (htmlPart?.body?.data) {
+      body = Buffer.from(htmlPart.body.data, 'base64').toString('utf-8');
+      isHtml = true;
+    }
   }
📝 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
// Decode the message body
let body = '';
let isHtml = false;
const payload = data.message?.payload;
if (payload) {
if (payload.mimeType === 'text/plain' && payload.body?.data) {
body = Buffer.from(payload.body.data, 'base64').toString('utf-8');
} else if (payload.mimeType === 'text/html' && payload.body?.data) {
body = Buffer.from(payload.body.data, 'base64').toString('utf-8');
isHtml = true;
} else if (payload.parts) {
// Prefer plain text, fall back to HTML
const textPart = payload.parts.find((p) => p.mimeType === 'text/plain');
const htmlPart = payload.parts.find((p) => p.mimeType === 'text/html');
if (textPart?.body?.data) {
body = Buffer.from(textPart.body.data, 'base64').toString('utf-8');
} else if (htmlPart?.body?.data) {
body = Buffer.from(htmlPart.body.data, 'base64').toString('utf-8');
isHtml = true;
}
}
}
const findPartByMime = (
part: gmail_v1.Schema$MessagePart | undefined,
mimeType: 'text/plain' | 'text/html'
): gmail_v1.Schema$MessagePart | undefined => {
if (!part) return undefined;
if (part.mimeType === mimeType && part.body?.data) return part;
for (const child of part.parts ?? []) {
const found = findPartByMime(child, mimeType);
if (found) return found;
}
return undefined;
};
// Decode the message body
let body = '';
let isHtml = false;
const payload = data.message?.payload;
if (payload) {
const textPart = findPartByMime(payload, 'text/plain');
const htmlPart = findPartByMime(payload, 'text/html');
if (textPart?.body?.data) {
body = Buffer.from(textPart.body.data, 'base64').toString('utf-8');
} else if (htmlPart?.body?.data) {
body = Buffer.from(htmlPart.body.data, 'base64').toString('utf-8');
isHtml = true;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/gmail/drafts.ts` around lines 151 - 173, The MIME parsing only
checks payload and first-level payload.parts so nested multipart structures are
missed; update the logic in the drafts parsing function (where payload,
payload.parts, body and isHtml are handled) to perform a recursive traversal of
parts: write a helper (e.g., findPartRecursive or getBodyFromParts) that walks
parts at any depth, looks for a text/plain part first and returns its decoded
body, otherwise falls back to text/html and sets isHtml=true; use that helper to
set body/isHtml instead of directly accessing payload.parts, and keep existing
base64 decoding behavior for part.body.data.

}

context.performanceMonitor.track('gmail:getDraft', Date.now() - context.startTime);
context.logger.info('Retrieved draft', { draftId, subject });
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

Avoid logging draft subjects at info level.

subject can contain sensitive user content. Logging it at info level creates avoidable privacy/compliance exposure.

🔒 Proposed fix (log identifiers only)
-  context.logger.info('Retrieved draft', { draftId, subject });
+  context.logger.info('Retrieved draft', { draftId });
...
-  context.logger.info('Updated draft', { draftId, subject });
+  context.logger.info('Updated draft', { draftId });

Also applies to: 251-251

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

In `@src/modules/gmail/drafts.ts` at line 176, The code logs sensitive user
content by calling context.logger.info with the draft subject; remove the
subject from info-level logs and log only non-sensitive identifiers (e.g.,
draftId) instead. Locate the context.logger.info calls in
src/modules/gmail/drafts.ts (the "Retrieved draft" log and the similar
occurrence later) and change them to log only draftId (or masked/hashed
identifiers) at info level; if you still need the subject for troubleshooting,
send it to a debug-level log (context.logger.debug) or store it in a
secure/audited location rather than info.

Comment thread src/modules/gmail/drafts.ts Outdated
ESLint 'curly' rule requires block braces for all if bodies.
Fix the 6 single-line ifs in drafts.ts updateDraft function.
@github-actions
Copy link
Copy Markdown

📊 Type Coverage Report

Type Coverage: 98.63%

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

@AojdevStudio AojdevStudio merged commit 5fc342f into main Mar 28, 2026
32 of 33 checks passed
@AojdevStudio AojdevStudio deleted the feature/gdrive-11-gmail-draft-management branch March 28, 2026 21:01
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.

feat(gmail): add listDrafts, getDraft, updateDraft, and deleteDraft operations

1 participant