Skip to content

Gmail

Overview#

The Gmail integration connects Cognest to Google's Gmail API using OAuth 2.0. It supports sending and replying to emails, searching across the mailbox, managing labels, reading threads, and receiving real-time notifications for new messages via Google Cloud Pub/Sub push notifications. All operations respect Gmail's scoped permissions model.

Prerequisites#

  1. A Google Cloud project with the Gmail API enabled
  2. OAuth 2.0 credentials (Client ID and Client Secret) from the Google Cloud Console
  3. Authorized redirect URI set to your Cognest instance's OAuth callback endpoint
  4. For real-time notifications: a Google Cloud Pub/Sub topic and subscription configured for Gmail push

OAuth Scopes

Cognest requests the following Gmail scopes by default: gmail.send, gmail.readonly, gmail.modify, and gmail.labels. You can restrict scopes in the configuration if your use case requires fewer permissions. The user will be prompted to authorize these scopes during the OAuth consent flow.

Configuration#

Add the Gmail integration to your cognest.config.yaml:

cognest.config.yaml
integrations:
  gmail:
    enabled: true
    credentials:
      client_id: ${GMAIL_CLIENT_ID}
      client_secret: ${GMAIL_CLIENT_SECRET}
    settings:
      oauth_callback_path: "/auth/google/callback"
      token_storage: "encrypted-file"
      scopes:
        - gmail.send
        - gmail.readonly
        - gmail.modify
        - gmail.labels
      watch:
        enabled: true
        topic: "projects/my-project/topics/cognest-gmail"
        label_filter: "INBOX"
      max_results: 50
      signature: "Sent via Cognest"

Store your credentials in environment variables:

.env
# .env
GMAIL_CLIENT_ID=829471035284-abc123def456.apps.googleusercontent.com
GMAIL_CLIENT_SECRET=GOCSPX-aBcDeFgHiJkLmNoPqRs

SDK Usage#

Register the integration programmatically for full control over the configuration:

index.ts
import { Cognest } from '@cognest/sdk'
import { Gmail } from '@cognest/integrations/gmail'

const cognest = new Cognest({ configPath: false })

cognest.use(Gmail, {
  clientId: process.env.GMAIL_CLIENT_ID!,
  clientSecret: process.env.GMAIL_CLIENT_SECRET!,
  settings: {
    oauthCallbackPath: '/auth/google/callback',
    tokenStorage: 'encrypted-file',
    scopes: [
      'gmail.send',
      'gmail.readonly',
      'gmail.modify',
      'gmail.labels',
    ],
    watch: {
      enabled: true,
      topic: 'projects/my-project/topics/cognest-gmail',
      labelFilter: 'INBOX',
    },
    maxResults: 50,
    signature: 'Sent via Cognest',
  },
})

await cognest.start()

OAuth Flow#

The Gmail integration requires an OAuth 2.0 authorization flow. Cognest handles token refresh automatically. To initiate the flow:

auth.ts
const gmail = cognest.integration<Gmail>('gmail')

// Generate the authorization URL
const authUrl = gmail.getAuthUrl({
  redirectUri: 'https://your-app.com/auth/google/callback',
  accessType: 'offline',   // Required for refresh tokens
  prompt: 'consent',       // Force consent to get refresh token
})

// After user authorizes, exchange the code
await gmail.handleCallback(authorizationCode)

// Tokens are now stored and auto-refreshed

Available Methods#

Access Gmail methods through cognest.integration('gmail'):

MethodParametersDescription
`send(options)``{ to, subject, body, cc?, bcc?, attachments?, html? }`Send a new email to one or more recipients
`reply(threadId, options)``threadId`: thread to reply to, `{ body, html?, attachments? }`Reply to an existing email thread
`getThread(threadId, options?)``threadId`: the Gmail thread IDRetrieve the full thread with all messages and metadata
`search(query, options?)``query`: Gmail search syntax (e.g. `from:user@example.com`)Search the mailbox using Gmail's query operators
`addLabel(messageId, labelId)``messageId`: target message, `labelId`: label to applyAdd a label to a specific message
`removeLabel(messageId, labelId)``messageId`: target message, `labelId`: label to removeRemove a label from a specific message
`getUnread(options?)``{ maxResults?, labelIds?, after? }`Fetch unread messages, optionally filtered by label or date

Events#

Events are delivered via Google Cloud Pub/Sub push notifications. Enable the watch setting to receive real-time notifications:

EventPayloadDescription
`email:received``{ messageId, threadId, from, to, subject, snippet, labels, date }`A new email arrived in the watched mailbox
`email:thread_reply``{ messageId, threadId, from, to, subject, snippet, inReplyTo }`A new reply was added to an existing thread

Example#

A complete example that monitors incoming email, generates AI responses, and manages labels:

support-bot.ts
import { Cognest } from '@cognest/sdk'
import { Gmail } from '@cognest/integrations/gmail'

const cognest = new Cognest()
const gmail = cognest.integration<Gmail>('gmail')

// Auto-respond to new support emails
cognest.on('gmail:email:received', async (ctx) => {
  const { messageId, threadId, from, subject, snippet, labels } = ctx.payload

  // Only process emails in the support label
  if (!labels.includes('Label_support')) return

  // Skip if the sender is our own address (avoid loops)
  const profile = await gmail.getProfile()
  if (from.includes(profile.emailAddress)) return

  // Fetch the full thread for context
  const thread = await gmail.getThread(threadId, {
    format: 'full',
  })

  // Build conversation history from thread messages
  const history = thread.messages.map((msg) => ({
    role: msg.from.includes(profile.emailAddress) ? 'assistant' as const : 'user' as const,
    content: msg.textBody,
  }))

  // Generate a response with the AI engine
  const response = await cognest.engine.chat({
    conversationId: `email:${threadId}`,
    messages: history,
    systemPrompt: `You are a helpful customer support agent for Acme Corp.
      Be concise and professional. If you cannot resolve the issue,
      say you will escalate to a human team member.`,
  })

  // Reply to the thread
  await gmail.reply(threadId, {
    body: response.text,
    html: `<div style="font-family: sans-serif;">${response.html}</div>`,
  })

  // Label the message as processed
  await gmail.addLabel(messageId, 'Label_auto_replied')
})

// Periodic check for unread messages (backup for missed webhooks)
cognest.schedule('*/15 * * * *', async () => {
  const unread = await gmail.getUnread({
    maxResults: 10,
    labelIds: ['Label_support'],
  })

  for (const msg of unread.messages) {
    // Emit as if received via webhook
    cognest.emit('gmail:email:received', {
      messageId: msg.id,
      threadId: msg.threadId,
      from: msg.from,
      to: msg.to,
      subject: msg.subject,
      snippet: msg.snippet,
      labels: msg.labelIds,
      date: msg.date,
    })
  }
})

// Search for specific emails
async function findInvoices(after: string) {
  const results = await gmail.search(
    `subject:invoice has:attachment after:${after}`,
    { maxResults: 25 }
  )

  return results.messages.map((msg) => ({
    id: msg.id,
    from: msg.from,
    subject: msg.subject,
    date: msg.date,
  }))
}

await cognest.start()
console.log('Gmail integration is running')

Troubleshooting#

Common Issues

OAuth consent screen stuck in testing: If only test users can authorize, publish your OAuth consent screen in the Google Cloud Console or add the target email as a test user. No events received: Ensure your Pub/Sub topic has the correct IAM permissions. The Gmail service account (gmail-api-push@system.gserviceaccount.com) needs the Pub/Sub Publisher role on your topic. Token refresh fails with invalid_grant: The refresh token was revoked or expired. This happens if the user changes their password, revokes access in Google Account settings, or if the token is unused for 6 months. Trigger a new OAuth flow to get a fresh token. Rate limit errors (429): Gmail API has a per-user rate limit of 250 quota units per second. Batch your requests and implement exponential backoff. The SDK handles basic retries automatically but sustained high-volume sending may require a Google Workspace account with higher quotas.