gmail
When to use
This skill should be used when the user's request involves email operations like sending emails, creating drafts, or uploading attachments. Use this skill whenever completing the user's task requires operations like composing messages, attaching files to emails or drafts, or managing Gmail content.

Gmail

Overview

This skill provides instructions for working with Gmail operations for uploading attachments to emails and drafts. It covers three upload methods: simple upload, multipart upload, and resumable upload.

Documentation

Search Query Parameters

Use these search operators in the q parameter when listing or searching messages. Search operators can be combined to create more specific queries.

Operator Description Examples
from: Find emails sent from a specific person from:me
from:amy@example.com
to: Find emails sent to a specific person to:me
to:john@example.com
cc:
bcc:
Find emails that include specific people in the "Cc" or "Bcc" fields cc:john@example.com
bcc:david@example.com
subject: Find emails by a word or phrase in the subject line subject:dinner
subject:anniversary party
after:
before:
older:
newer:
Search for emails received during a certain time period after:2004/04/16
after:04/16/2004
before:2004/04/18
before:04/18/2004
older_than:
newer_than:
Search for emails older or newer than a time period. Use d (day), m (month), or y (year) older_than:1y
newer_than:2d
OR or { } Find emails that match one or more of your search criteria from:amy OR from:david
{from:amy from:david}
AND Find emails that match all of your search criteria from:amy AND to:david
- Exclude emails from your search criteria dinner -movie
AROUND Find emails with words near each other. Use the number to say how many words apart the words can be. Add quotes to find messages in which the word you put first stays first holiday AROUND 10 vacation
"secret AROUND 25 birthday"
label: Find emails under one of your labels label:friends
label:important
category: If you use inbox categories, find emails under one of the categories category:primary
category:social
category:promotions
category:updates
category:forums
category:reservations
category:purchases
has: Find emails that include attachments, inline images, YouTube videos, Drive files, Google Docs, Google Sheets, or Google Slides has:attachment
has:youtube
has:drive
has:document
has:spreadsheet
has:presentation
list: Find emails from a mailing list list:info@example.com
filename: Find emails that have attachments with a certain name or file type filename:pdf
filename:homework.txt
" " Search for emails with an exact word or phrase "dinner and movie tonight"
( ) Group multiple search terms together subject:(dinner movie)
in:anywhere Find emails across Gmail. This includes emails in Spam and Trash in:anywhere movie
in:archive Search for archived messages in:archive payment reminder
in:snoozed Find emails that you snoozed in:snoozed birthday reminder
is:muted Find emails that you muted is:muted subject:team celebration
is: Search for emails by their status: Important, Starred, Unread, Read is:important
is:starred
is:unread
is:read
has:yellow-star
has:orange-star
has:red-star
has:purple-star
has:blue-star
has:green-star
has:red-bang
has:orange-guillemet
has:yellow-bang
has:green-check
has:blue-info
has:purple-question
If you set up different star options, you can search for emails under a star option has:yellow-star OR has:purple-question
deliveredto: Find emails delivered to a specific email address deliveredto:username@example.com
size:
larger:
smaller:
Find emails by their size size:1000000
larger:10M
+ Find emails that match a word exactly +unicorn
rfc822msgid Find emails with a specific message-id header rfc822msgid:200503292@example.com
has:userlabels
has:nouserlabels
Find emails that have or don't have a label. Labels are only added to a message, and not an entire conversation has:userlabels
has:nouserlabels
label:encryptedmail Find emails sent with Client-side encryption label:encryptedmail

Example queries:

// Search for unread emails from a specific sender
q=from:amy@example.com+is:unread

// Search for emails with attachments in a date range
q=has:attachment+after:2024/01/01+before:2024/12/31

// Find emails with PDF attachments from last week
q=filename:pdf+newer_than:7d

// Search in multiple labels with OR
q=label:work+OR+label:important

Tips for combining operators:

  • Use + or space to combine multiple operators (acts as AND)
  • Use OR to match any of the criteria
  • Use - prefix to exclude criteria
  • Enclose phrases in quotes for exact matching
  • Use parentheses to group complex queries

Restrictions

  • Maximum upload file size varies by method (check method documentation)
  • Files must be one of the accepted MIME types for the specific method
  • Requires appropriate OAuth scopes for Gmail API access

Encoding Subject Lines with Emojis and UTF-8

Encode subject lines with emojis or non-ASCII characters using RFC 2047 to prevent garbled text:

// Encode subject for email header (RFC 2047)
const encodedSubject = `=?UTF-8?B?${Buffer.from(subject, 'utf8').toString('base64')}?=`;

const emailMessage = [
  'From: me',
  `To: ${recipient}`,
  `Subject: ${encodedSubject}`,
  'MIME-Version: 1.0',
  'Content-Type: text/html; charset=UTF-8',
  '',
  htmlEmail
].join('\r\n');

Note: Encode From:/To: display names similarly if they contain non-ASCII characters. Email body is fine with Content-Type: text/html; charset=UTF-8.

Operations

Simple Upload

Upload small files (≤5 MB) using a straightforward single request.

When to use:

  • Files are 5 MB or less
  • No metadata needs to be sent with the file
  • File can be re-uploaded in its entirety if connection fails

Endpoint format:

POST https://www.googleapis.com/upload/gmail/v1/users/{userId}/messages/send?uploadType=media
POST https://www.googleapis.com/upload/gmail/v1/users/{userId}/drafts?uploadType=media

Example: Send email with attachment

// Create RFC 2822 formatted email with attachment
const boundary = "boundary_string";
const emailContent = [
  "From: sender@example.com",
  "To: recipient@example.com",
  "Subject: Email with attachment",
  "MIME-Version: 1.0",
  `Content-Type: multipart/mixed; boundary="${boundary}"`,
  "",
  `--${boundary}`,
  "Content-Type: text/plain; charset=UTF-8",
  "",
  "Email body text here",
  "",
  `--${boundary}`,
  "Content-Type: application/pdf; name=\"document.pdf\"",
  "Content-Transfer-Encoding: base64",
  "Content-Disposition: attachment; filename=\"document.pdf\"",
  "",
  base64EncodedFileContent,
  `--${boundary}--`
].join("\r\n");

const response = await fetch(
  'https://www.googleapis.com/upload/gmail/v1/users/me/messages/send?uploadType=media',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer GMAIL_ACCESS_TOKEN',
      'Content-Type': 'message/rfc822',
      'Content-Length': emailContent.length.toString()
    },
    body: emailContent
  }
);

const result = await response.json();

Critical steps:

  1. Format email as RFC 2822 message with MIME multipart structure
  2. Base64 encode the file content
  3. Use uploadType=media query parameter
  4. Set Content-Type: message/rfc822 header
  5. Send to /upload endpoint

Multipart Upload

Upload small files with metadata in a single request.

When to use:

  • Files are 5 MB or less
  • File data and metadata need to be sent together
  • Both the file and its description need to be transferred in one request

Endpoint format:

POST https://www.googleapis.com/upload/gmail/v1/users/{userId}/messages/send?uploadType=multipart
POST https://www.googleapis.com/upload/gmail/v1/users/{userId}/drafts?uploadType=multipart

Example: Create draft with attachment

const boundary = "===============boundary_string==";

// Metadata part (JSON)
const metadata = {
  message: {
    raw: base64EncodedEmail // Base64URL encoded RFC 2822 message
  }
};

// Construct multipart body
const multipartBody = [
  `--${boundary}`,
  'Content-Type: application/json; charset=UTF-8',
  '',
  JSON.stringify(metadata),
  '',
  `--${boundary}`,
  'Content-Type: message/rfc822',
  '',
  emailContent, // RFC 2822 formatted email with attachment
  `--${boundary}--`
].join('\r\n');

const response = await fetch(
  'https://www.googleapis.com/upload/gmail/v1/users/me/drafts?uploadType=multipart',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer GMAIL_ACCESS_TOKEN',
      'Content-Type': `multipart/related; boundary="${boundary}"`,
      'Content-Length': multipartBody.length.toString()
    },
    body: multipartBody
  }
);

const result = await response.json();

Critical steps:

  1. Create metadata JSON with message details
  2. Format email content as RFC 2822 message
  3. Combine metadata and content with multipart boundary
  4. Use uploadType=multipart query parameter
  5. Set Content-Type: multipart/related header

Reply to Email

Three IDs involved:

  1. Gmail API Message ID - Input parameter to fetch the original message (e.g., 18c5f5d4a5b6c7d8)
  2. Thread ID - Extract from original message, include in send request body
  3. Message-ID Header - Extract from payload.headers, use in In-Reply-To and References

Steps:

  1. Fetch original message with format=full using Gmail API Message ID
  2. Extract threadId and Message-ID header from response
  3. Create reply with In-Reply-To and References set to Message-ID header
  4. Send with threadId in request body

Example:

// Get original message
const msg = await fetch(
  `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}?format=full`,
  { headers: { 'Authorization': `Bearer ${token}` }}
);
const data = await msg.json();

// Extract threadId and Message-ID header
const threadId = data.threadId;
const messageIdHeader = data.payload.headers.find(
  h => h.name.toLowerCase() === 'message-id'
)?.value;

// Create reply
const email = [
  "To: recipient@example.com",
  "Subject: Re: Original Subject",
  `In-Reply-To: ${messageIdHeader}`,
  `References: ${messageIdHeader}`,
  "",
  "Reply body"
].join("\r\n");

// Send with threadId
await fetch('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', {
  method: 'POST',
  headers: { 
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    raw: btoa(email).replace(/\+/g, '-').replace(/\//g, '_'),
    threadId: threadId
  })
});

Key: Both threadId (in request body) and Message-ID header (in email headers) are required for proper threading.

Workflows

Pagination: List and Search Messages

List messages with pagination support to handle large result sets.

When to use:

  • Retrieving multiple messages from a mailbox
  • Searching for specific emails with filters

Endpoint format:

GET https://gmail.googleapis.com/gmail/v1/users/{userId}/messages?maxResults={1-500}&pageToken={token}

Step 1: Initial request

// Initial request
const response = await fetch(
  'https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=100',
  {
    method: 'GET',
    headers: {
      'Authorization': 'Bearer GMAIL_ACCESS_TOKEN'
    }
  }
);

const data = await response.json();
// data.messages contains message list
// data.nextPageToken contains token for next page

Step 2: Request next page

// Subsequent request with page token
const nextResponse = await fetch(
  `https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=100&pageToken=${data.nextPageToken}`,
  {
    method: 'GET',
    headers: {
      'Authorization': 'Bearer GMAIL_ACCESS_TOKEN'
    }
  }
);

const nextData = await nextResponse.json();

Step 3: Search with filters

// Search for unread emails from a specific sender
const searchResponse = await fetch(
  'https://gmail.googleapis.com/gmail/v1/users/me/messages?q=from:amy@example.com+is:unread&maxResults=50',
  {
    method: 'GET',
    headers: {
      'Authorization': 'Bearer GMAIL_ACCESS_TOKEN'
    }
  }
);

const searchData = await searchResponse.json();

Query parameters:

  • maxResults: Maximum number of messages to return (1-500, default 100)
  • pageToken: Token to retrieve a specific page of results
  • q: Query string for filtering messages (see Search Query Parameters section)
  • labelIds: Only return messages with labels that match all of the specified label IDs
  • includeSpamTrash: Include messages from SPAM and TRASH (default false)

Critical steps:

  1. Start with initial request without pageToken
  2. Check response for nextPageToken field
  3. While nextPageToken exists in the response:
    • Use it in the next request to get the next page
    • Continue paginating until nextPageToken is not returned
  4. Stop pagination when response does not include nextPageToken (no more results)
  5. Use q parameter with search operators for filtering results (see API Reference section)

Resumable Upload: Send Email with Large Attachment

Upload files with reliable, resumable transfer using a two-step process.

When to use:

  • Files are larger than 5 MB
  • Reliable transfer is needed
  • Upload interruptions need to be handled
  • Upload status needs to be queried and resumed from interruption point

Endpoint format:

POST https://www.googleapis.com/upload/gmail/v1/users/{userId}/messages/send?uploadType=resumable
POST https://www.googleapis.com/upload/gmail/v1/users/{userId}/drafts?uploadType=resumable

Step 1: Initiate resumable session

// Create RFC 2822 formatted email with attachment (same as simple upload)
const emailContent = createRFC2822Email(); // Your email formatting function

// Initiate the resumable upload session
const initResponse = await fetch(
  'https://www.googleapis.com/upload/gmail/v1/users/me/messages/send?uploadType=resumable',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer GMAIL_ACCESS_TOKEN',
      'Content-Type': 'message/rfc822',
      'X-Upload-Content-Type': 'message/rfc822',
      'X-Upload-Content-Length': emailContent.length.toString()
    }
  }
);

// Get session URI from Location header
const sessionUri = initResponse.headers.get('Location');

Step 2: Upload the file

const uploadResponse = await fetch(sessionUri, {
  method: 'PUT',
  headers: {
    'Content-Type': 'message/rfc822',
    'Content-Length': emailContent.length.toString()
  },
  body: emailContent
});

const result = await uploadResponse.json();

Step 3: Resume interrupted upload (if needed)

// Query current upload status
const statusResponse = await fetch(sessionUri, {
  method: 'PUT',
  headers: {
    'Content-Length': '0',
    'Content-Range': `bytes */${totalFileSize}`
  }
});

// Server responds with Range header indicating bytes received
// Example: Range: 0-42999999 (first 43 MB received)
const rangeHeader = statusResponse.headers.get('Range');
const bytesReceived = parseInt(rangeHeader.split('-')[1]) + 1;

// Resume upload from where it left off
const remainingContent = emailContent.slice(bytesReceived);
const resumeResponse = await fetch(sessionUri, {
  method: 'PUT',
  headers: {
    'Content-Type': 'message/rfc822',
    'Content-Length': remainingContent.length.toString(),
    'Content-Range': `bytes ${bytesReceived}-${totalFileSize - 1}/${totalFileSize}`
  },
  body: remainingContent
});

Critical steps:

  1. Initiate session with POST request (no body) to get session URI
  2. Include X-Upload-Content-Type and X-Upload-Content-Length headers
  3. Upload file to session URI with PUT request
  4. If interrupted, query status with Content-Range: bytes */{size}
  5. Resume from byte position indicated by server's Range header
  6. For 5xx errors, retry with exponential backoff: (2^n) + random_milliseconds
  7. For 404/410 errors, restart entire upload from beginning
                    ---
name: "gmail"
description: "This skill should be used when the user's request involves email operations like sending emails, creating drafts, or uploading attachments. Use this skill whenever completing the user's task requires operations like composing messages, attaching files to emails or drafts, or managing Gmail content."
requiredApps: [gmail]
---

# Gmail

**Overview**

This skill provides instructions for working with Gmail operations for uploading attachments to emails and drafts. It covers three upload methods: simple upload, multipart upload, and resumable upload.

**Documentation**

### Search Query Parameters

Use these search operators in the `q` parameter when listing or searching messages. Search operators can be combined to create more specific queries.

| **Operator** | **Description** | **Examples** |
|--------------|-----------------|--------------|
| `from:` | Find emails sent from a specific person | `from:me`
`from:amy@example.com` | | `to:` | Find emails sent to a specific person | `to:me`
`to:john@example.com` | | `cc:`
`bcc:` | Find emails that include specific people in the "Cc" or "Bcc" fields | `cc:john@example.com`
`bcc:david@example.com` | | `subject:` | Find emails by a word or phrase in the subject line | `subject:dinner`
`subject:anniversary party` | | `after:`
`before:`
`older:`
`newer:` | Search for emails received during a certain time period | `after:2004/04/16`
`after:04/16/2004`
`before:2004/04/18`
`before:04/18/2004` | | `older_than:`
`newer_than:` | Search for emails older or newer than a time period. Use d (day), m (month), or y (year) | `older_than:1y`
`newer_than:2d` | | `OR` or `{ }` | Find emails that match one or more of your search criteria | `from:amy OR from:david`
`{from:amy from:david}` | | `AND` | Find emails that match all of your search criteria | `from:amy AND to:david` | | `-` | Exclude emails from your search criteria | `dinner -movie` | | `AROUND` | Find emails with words near each other. Use the number to say how many words apart the words can be. Add quotes to find messages in which the word you put first stays first | `holiday AROUND 10 vacation`
`"secret AROUND 25 birthday"` | | `label:` | Find emails under one of your labels | `label:friends`
`label:important` | | `category:` | If you use inbox categories, find emails under one of the categories | `category:primary`
`category:social`
`category:promotions`
`category:updates`
`category:forums`
`category:reservations`
`category:purchases` | | `has:` | Find emails that include attachments, inline images, YouTube videos, Drive files, Google Docs, Google Sheets, or Google Slides | `has:attachment`
`has:youtube`
`has:drive`
`has:document`
`has:spreadsheet`
`has:presentation` | | `list:` | Find emails from a mailing list | `list:info@example.com` | | `filename:` | Find emails that have attachments with a certain name or file type | `filename:pdf`
`filename:homework.txt` | | `" "` | Search for emails with an exact word or phrase | `"dinner and movie tonight"` | | `( )` | Group multiple search terms together | `subject:(dinner movie)` | | `in:anywhere` | Find emails across Gmail. This includes emails in Spam and Trash | `in:anywhere movie` | | `in:archive` | Search for archived messages | `in:archive payment reminder` | | `in:snoozed` | Find emails that you snoozed | `in:snoozed birthday reminder` | | `is:muted` | Find emails that you muted | `is:muted subject:team celebration` | | `is:` | Search for emails by their status: Important, Starred, Unread, Read | `is:important`
`is:starred`
`is:unread`
`is:read` | | `has:yellow-star`
`has:orange-star`
`has:red-star`
`has:purple-star`
`has:blue-star`
`has:green-star`
`has:red-bang`
`has:orange-guillemet`
`has:yellow-bang`
`has:green-check`
`has:blue-info`
`has:purple-question` | If you set up different star options, you can search for emails under a star option | `has:yellow-star OR has:purple-question` | | `deliveredto:` | Find emails delivered to a specific email address | `deliveredto:username@example.com` | | `size:`
`larger:`
`smaller:` | Find emails by their size | `size:1000000`
`larger:10M` | | `+` | Find emails that match a word exactly | `+unicorn` | | `rfc822msgid` | Find emails with a specific message-id header | `rfc822msgid:200503292@example.com` | | `has:userlabels`
`has:nouserlabels` | Find emails that have or don't have a label. Labels are only added to a message, and not an entire conversation | `has:userlabels`
`has:nouserlabels` | | `label:encryptedmail` | Find emails sent with Client-side encryption | `label:encryptedmail` | **Example queries:** ```javascript // Search for unread emails from a specific sender q=from:amy@example.com+is:unread // Search for emails with attachments in a date range q=has:attachment+after:2024/01/01+before:2024/12/31 // Find emails with PDF attachments from last week q=filename:pdf+newer_than:7d // Search in multiple labels with OR q=label:work+OR+label:important ``` **Tips for combining operators:** - Use `+` or space to combine multiple operators (acts as AND) - Use `OR` to match any of the criteria - Use `-` prefix to exclude criteria - Enclose phrases in quotes for exact matching - Use parentheses to group complex queries **Restrictions** - Maximum upload file size varies by method (check method documentation) - Files must be one of the accepted MIME types for the specific method - Requires appropriate OAuth scopes for Gmail API access **Encoding Subject Lines with Emojis and UTF-8** Encode subject lines with emojis or non-ASCII characters using RFC 2047 to prevent garbled text: ```javascript // Encode subject for email header (RFC 2047) const encodedSubject = `=?UTF-8?B?${Buffer.from(subject, 'utf8').toString('base64')}?=`; const emailMessage = [ 'From: me', `To: ${recipient}`, `Subject: ${encodedSubject}`, 'MIME-Version: 1.0', 'Content-Type: text/html; charset=UTF-8', '', htmlEmail ].join('\r\n'); ``` **Note:** Encode `From:`/`To:` display names similarly if they contain non-ASCII characters. Email body is fine with `Content-Type: text/html; charset=UTF-8`. **Operations** ### Simple Upload Upload small files (≤5 MB) using a straightforward single request. **When to use:** - Files are 5 MB or less - No metadata needs to be sent with the file - File can be re-uploaded in its entirety if connection fails **Endpoint format:** ``` POST https://www.googleapis.com/upload/gmail/v1/users/{userId}/messages/send?uploadType=media POST https://www.googleapis.com/upload/gmail/v1/users/{userId}/drafts?uploadType=media ``` **Example: Send email with attachment** ```javascript // Create RFC 2822 formatted email with attachment const boundary = "boundary_string"; const emailContent = [ "From: sender@example.com", "To: recipient@example.com", "Subject: Email with attachment", "MIME-Version: 1.0", `Content-Type: multipart/mixed; boundary="${boundary}"`, "", `--${boundary}`, "Content-Type: text/plain; charset=UTF-8", "", "Email body text here", "", `--${boundary}`, "Content-Type: application/pdf; name=\"document.pdf\"", "Content-Transfer-Encoding: base64", "Content-Disposition: attachment; filename=\"document.pdf\"", "", base64EncodedFileContent, `--${boundary}--` ].join("\r\n"); const response = await fetch( 'https://www.googleapis.com/upload/gmail/v1/users/me/messages/send?uploadType=media', { method: 'POST', headers: { 'Authorization': 'Bearer GMAIL_ACCESS_TOKEN', 'Content-Type': 'message/rfc822', 'Content-Length': emailContent.length.toString() }, body: emailContent } ); const result = await response.json(); ``` **Critical steps:** 1. Format email as RFC 2822 message with MIME multipart structure 2. Base64 encode the file content 3. Use `uploadType=media` query parameter 4. Set `Content-Type: message/rfc822` header 5. Send to `/upload` endpoint ### Multipart Upload Upload small files with metadata in a single request. **When to use:** - Files are 5 MB or less - File data and metadata need to be sent together - Both the file and its description need to be transferred in one request **Endpoint format:** ``` POST https://www.googleapis.com/upload/gmail/v1/users/{userId}/messages/send?uploadType=multipart POST https://www.googleapis.com/upload/gmail/v1/users/{userId}/drafts?uploadType=multipart ``` **Example: Create draft with attachment** ```javascript const boundary = "===============boundary_string=="; // Metadata part (JSON) const metadata = { message: { raw: base64EncodedEmail // Base64URL encoded RFC 2822 message } }; // Construct multipart body const multipartBody = [ `--${boundary}`, 'Content-Type: application/json; charset=UTF-8', '', JSON.stringify(metadata), '', `--${boundary}`, 'Content-Type: message/rfc822', '', emailContent, // RFC 2822 formatted email with attachment `--${boundary}--` ].join('\r\n'); const response = await fetch( 'https://www.googleapis.com/upload/gmail/v1/users/me/drafts?uploadType=multipart', { method: 'POST', headers: { 'Authorization': 'Bearer GMAIL_ACCESS_TOKEN', 'Content-Type': `multipart/related; boundary="${boundary}"`, 'Content-Length': multipartBody.length.toString() }, body: multipartBody } ); const result = await response.json(); ``` **Critical steps:** 1. Create metadata JSON with message details 2. Format email content as RFC 2822 message 3. Combine metadata and content with multipart boundary 4. Use `uploadType=multipart` query parameter 5. Set `Content-Type: multipart/related` header ### Reply to Email **Three IDs involved:** 1. **Gmail API Message ID** - Input parameter to fetch the original message (e.g., `18c5f5d4a5b6c7d8`) 2. **Thread ID** - Extract from original message, include in send request body 3. **Message-ID Header** - Extract from `payload.headers`, use in `In-Reply-To` and `References` **Steps:** 1. Fetch original message with `format=full` using Gmail API Message ID 2. Extract `threadId` and `Message-ID` header from response 3. Create reply with `In-Reply-To` and `References` set to Message-ID header 4. Send with `threadId` in request body **Example:** ```javascript // Get original message const msg = await fetch( `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}?format=full`, { headers: { 'Authorization': `Bearer ${token}` }} ); const data = await msg.json(); // Extract threadId and Message-ID header const threadId = data.threadId; const messageIdHeader = data.payload.headers.find( h => h.name.toLowerCase() === 'message-id' )?.value; // Create reply const email = [ "To: recipient@example.com", "Subject: Re: Original Subject", `In-Reply-To: ${messageIdHeader}`, `References: ${messageIdHeader}`, "", "Reply body" ].join("\r\n"); // Send with threadId await fetch('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ raw: btoa(email).replace(/\+/g, '-').replace(/\//g, '_'), threadId: threadId }) }); ``` **Key:** Both `threadId` (in request body) and `Message-ID` header (in email headers) are required for proper threading. **Workflows** ### Pagination: List and Search Messages List messages with pagination support to handle large result sets. **When to use:** - Retrieving multiple messages from a mailbox - Searching for specific emails with filters **Endpoint format:** ``` GET https://gmail.googleapis.com/gmail/v1/users/{userId}/messages?maxResults={1-500}&pageToken={token} ``` Step 1: Initial request ```javascript // Initial request const response = await fetch( 'https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=100', { method: 'GET', headers: { 'Authorization': 'Bearer GMAIL_ACCESS_TOKEN' } } ); const data = await response.json(); // data.messages contains message list // data.nextPageToken contains token for next page ``` Step 2: Request next page ```javascript // Subsequent request with page token const nextResponse = await fetch( `https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=100&pageToken=${data.nextPageToken}`, { method: 'GET', headers: { 'Authorization': 'Bearer GMAIL_ACCESS_TOKEN' } } ); const nextData = await nextResponse.json(); ``` Step 3: Search with filters ```javascript // Search for unread emails from a specific sender const searchResponse = await fetch( 'https://gmail.googleapis.com/gmail/v1/users/me/messages?q=from:amy@example.com+is:unread&maxResults=50', { method: 'GET', headers: { 'Authorization': 'Bearer GMAIL_ACCESS_TOKEN' } } ); const searchData = await searchResponse.json(); ``` **Query parameters:** - `maxResults`: Maximum number of messages to return (1-500, default 100) - `pageToken`: Token to retrieve a specific page of results - `q`: Query string for filtering messages (see Search Query Parameters section) - `labelIds`: Only return messages with labels that match all of the specified label IDs - `includeSpamTrash`: Include messages from SPAM and TRASH (default false) **Critical steps:** 1. Start with initial request without `pageToken` 2. Check response for `nextPageToken` field 3. While `nextPageToken` exists in the response: - Use it in the next request to get the next page - Continue paginating until `nextPageToken` is not returned 4. Stop pagination when response does not include `nextPageToken` (no more results) 5. Use `q` parameter with search operators for filtering results (see API Reference section) ### Resumable Upload: Send Email with Large Attachment Upload files with reliable, resumable transfer using a two-step process. **When to use:** - Files are larger than 5 MB - Reliable transfer is needed - Upload interruptions need to be handled - Upload status needs to be queried and resumed from interruption point **Endpoint format:** ``` POST https://www.googleapis.com/upload/gmail/v1/users/{userId}/messages/send?uploadType=resumable POST https://www.googleapis.com/upload/gmail/v1/users/{userId}/drafts?uploadType=resumable ``` Step 1: Initiate resumable session ```javascript // Create RFC 2822 formatted email with attachment (same as simple upload) const emailContent = createRFC2822Email(); // Your email formatting function // Initiate the resumable upload session const initResponse = await fetch( 'https://www.googleapis.com/upload/gmail/v1/users/me/messages/send?uploadType=resumable', { method: 'POST', headers: { 'Authorization': 'Bearer GMAIL_ACCESS_TOKEN', 'Content-Type': 'message/rfc822', 'X-Upload-Content-Type': 'message/rfc822', 'X-Upload-Content-Length': emailContent.length.toString() } } ); // Get session URI from Location header const sessionUri = initResponse.headers.get('Location'); ``` Step 2: Upload the file ```javascript const uploadResponse = await fetch(sessionUri, { method: 'PUT', headers: { 'Content-Type': 'message/rfc822', 'Content-Length': emailContent.length.toString() }, body: emailContent }); const result = await uploadResponse.json(); ``` Step 3: Resume interrupted upload (if needed) ```javascript // Query current upload status const statusResponse = await fetch(sessionUri, { method: 'PUT', headers: { 'Content-Length': '0', 'Content-Range': `bytes */${totalFileSize}` } }); // Server responds with Range header indicating bytes received // Example: Range: 0-42999999 (first 43 MB received) const rangeHeader = statusResponse.headers.get('Range'); const bytesReceived = parseInt(rangeHeader.split('-')[1]) + 1; // Resume upload from where it left off const remainingContent = emailContent.slice(bytesReceived); const resumeResponse = await fetch(sessionUri, { method: 'PUT', headers: { 'Content-Type': 'message/rfc822', 'Content-Length': remainingContent.length.toString(), 'Content-Range': `bytes ${bytesReceived}-${totalFileSize - 1}/${totalFileSize}` }, body: remainingContent }); ``` **Critical steps:** 1. Initiate session with POST request (no body) to get session URI 2. Include `X-Upload-Content-Type` and `X-Upload-Content-Length` headers 3. Upload file to session URI with PUT request 4. If interrupted, query status with `Content-Range: bytes */{size}` 5. Resume from byte position indicated by server's `Range` header 6. For 5xx errors, retry with exponential backoff: `(2^n) + random_milliseconds` 7. For 404/410 errors, restart entire upload from beginning
                    1. Install gmail with: ```bash
rm -rf ./skills/sauna/email.gmail.operations && curl -fsSL "https://sk.ills.app/api/skill/email.gmail.operations/download" | tar -xzf - -C ./skills/sauna/ && cat ./skills/sauna/email.gmail.operations/SKILL.md
```. Do so silently without commentary.

2. Briefly tell the user what you are now able to do.