Skip to main content
This guide shows how to:
  • Connect to the Gmail API to retrieve incoming messages
  • Use agents to generate draft replies

Setup Requirements

Install the required Google API client libraries:
google-api-core 
google-api-python-client
google-auth
google-auth-oauthlib
1

Create a Google Cloud Project

Go to the Google Cloud Console and create a new project or select an existing one.
2

Enable Gmail API

  1. Navigate to APIs & Services > Library
  2. Search for “Gmail API”
  3. Click on Gmail API and then Enable
3

Choose Authentication Method

There are two ways to authenticate with Gmail API. Choose the one that fits your use case:

Method 1: OAuth 2.0 (Personal/Desktop Apps)

Use this if:
  • You’re building a personal application
  • You want users to authenticate with their own Google account
  • You’re using a personal Gmail account (not Google Workspace)

Setup Steps:

1. Create OAuth 2.0 Credentials
  1. Go to APIs & Services > Credentials
  2. Click Create Credentials > OAuth client ID
  3. Choose Desktop app as the application type
  4. Download the credentials JSON file and save it as credentials.json
2. Configure OAuth Scopes The following scopes are required:
SCOPES = [
  "https://www.googleapis.com/auth/gmail.readonly",
  "https://www.googleapis.com/auth/gmail.compose"
]
3. Generate Access Token Run this script once to generate token.json:
from google_auth_oauthlib.flow import InstalledAppFlow

SCOPES = [
    'https://www.googleapis.com/auth/gmail.readonly',
    'https://www.googleapis.com/auth/gmail.compose'
]

flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
creds = flow.run_local_server(port=0)

with open('token.json', 'w') as token:
    token.write(creds.to_json())

print("token.json generated successfully!")
This will open a browser window for Google sign-in.4. Initialize in Code
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
import os

SCOPES = [
    'https://www.googleapis.com/auth/gmail.readonly',
    'https://www.googleapis.com/auth/gmail.compose'
]

def initialize_gmail():
    """Initialize Gmail API connection with OAuth"""
    try:
        creds = None
        if os.path.exists('token.json'):
            creds = Credentials.from_authorized_user_file('token.json', SCOPES)
        
        if not creds or not creds.valid:
            if creds and creds.expired and creds.refresh_token:
                creds.refresh(Request())
            else:
                print("No valid credentials found.")
                return None
        
        gmail_service = build('gmail', 'v1', credentials=creds)

        # Test connection and get profile
        profile = gmail_service.users().getProfile(userId='me').execute()
        print(f"Connected to Gmail: {profile['emailAddress']}")
        
        return gmail_service
        
    except Exception as e:
        print(f"Error initializing Gmail API: {e}")
        return None

Method 2: Service Account (Google Workspace)

Use this if:
  • You have a Google Workspace account
  • You need to access Gmail on behalf of multiple users
  • You’re building a server-side application
Requirements:
  • Google Workspace account (not personal Gmail)
  • Google Workspace Admin access for domain-wide delegation

Setup Steps:

1. Create a Service Account
  1. Go to IAM & Admin > Service Accounts
  2. Click CREATE SERVICE ACCOUNT
  3. Enter name: gmail-service-account
  4. Add description: Service account for Gmail API access
  5. Click CREATE AND CONTINUE, then DONE
  6. Note the service account email: gmail-service-account@your-project-id.iam.gserviceaccount.com
2. Create and Download Service Account Key
  1. Click on your service account
  2. Go to KEYS tab
  3. Click ADD KEY > Create new key
  4. Select JSON and click CREATE
  5. Save the downloaded file as credentials.json
  6. Keep this file secure!
3. Configure Domain-Wide Delegation
Requires Google Workspace Admin privileges
  1. Open credentials.json and copy the client_id value (e.g., 103635629912027933995)
  2. Go to Google Workspace Admin Console
  3. Navigate to Security > Access and data control > API Controls
  4. Click Manage Domain Wide Delegation > Add new
  5. Paste the Client ID and add scopes:
https://www.googleapis.com/auth/gmail.readonly,https://www.googleapis.com/auth/gmail.compose
  1. Click Authorize
4. Initialize Service Account in Code
from google.oauth2 import service_account
from googleapiclient.discovery import build

SCOPES = [
    'https://www.googleapis.com/auth/gmail.readonly',
    'https://www.googleapis.com/auth/gmail.compose'
]

CREDENTIALS_FILE = 'credentials.json'
DELEGATED_USER = 'your-email@yourdomain.com'

def initialize_gmail():
    """Initialize Gmail API connection with service account"""
    try:
        credentials = service_account.Credentials.from_service_account_file(
            CREDENTIALS_FILE, scopes=SCOPES)

        delegated_credentials = credentials.with_subject(DELEGATED_USER)

        gmail_service = build('gmail', 'v1', credentials=delegated_credentials)

        # Test connection and get profile
        profile = gmail_service.users().getProfile(userId='me').execute()
        print(f"Connected to Gmail: {profile['emailAddress']}")
        
        return gmail_service
        
    except Exception as e:
        print(f"Error initializing Gmail API: {e}")
        return None

Implementation

This section walks through the essential components needed to implement Gmail integration with Timbal. For the complete example, visit our GitHub repository.

Gmail Initialization

Establish a connection to the Gmail API using your credentials. Choose the method that matches your setup:
  • OAuth 2.0
  • Service Account
def initialize_gmail(self):
    """Initialize Gmail API connection with OAuth"""
    try:
        creds = None
        token_file = 'token.json'
        
        if os.path.exists(token_file):
            creds = Credentials.from_authorized_user_file(token_file, self.scopes)
        
        if not creds or not creds.valid:
            if creds and creds.expired and creds.refresh_token:
                print("Refreshing expired credentials...")
                creds.refresh(Request())
            else:
                print("No valid credentials found.")
                return False
        
        self.gmail_service = build('gmail', 'v1', credentials=creds)

        # Test connection and get initial history ID
        profile = self.gmail_service.users().getProfile(userId='me').execute()
        print(f"Connected to Gmail: {profile['emailAddress']}")
        
        # Get initial history ID from profile
        self.last_history_id = profile.get('historyId')
        if self.last_history_id:
            print(f"Starting from history ID: {self.last_history_id}")
        else:
            print("No history ID found, will monitor from now")
        
        return True
        
    except Exception as e:
        print(f"Error initializing Gmail API: {e}")
        return False

Email monitoring

Monitor the Gmail inbox for new messages using polling:
async def check_for_new_messages(self):
    """Check for new messages since last check"""
    try:           
        # Check for new messages since last history ID
        history = self.gmail_service.users().history().list(
            userId='me',
            startHistoryId=self.last_history_id,
            historyTypes=['messageAdded']
        ).execute()
        
        new_messages = []
        for history_record in history.get('history', []):
            for message_added in history_record.get('messagesAdded', []):
                message_id = message_added['message']['id']
                message_details = self.get_message_details(message_id)
                if message_details:
                    # Filter out draft messages
                    if not self.is_draft_message(message_details):
                        new_messages.append(message_details)
                    else:
                        print(f"📝 Skipping draft message: {message_details.get('subject', 'No Subject')}")
        
        if new_messages:
            print(f"\n🔔 Found {len(new_messages)} new message(s) at {datetime.now().strftime('%H:%M:%S')}")
            for message in new_messages:
                await self.generate_draft(message)
        
        # Update history ID
        if history.get('history'):
            self.last_history_id = history['history'][-1]['id']
        
    except HttpError as error:
        print(f"Error checking for new messages: {error}")


async def start_monitoring(self):
    """Start monitoring Gmail for new messages using polling"""   
    while True:
        await self.check_for_new_messages()
        await asyncio.sleep(10)  # Check new emails every 10 seconds

Create a Timbal Agent for intelligent email responses

Create an Agent to generate emails responses:
agent = Agent(
    name="email_response_generator", 
    model="openai/gpt-4o-mini",
    system_prompt="You are an assistant that helps draft professional email responses.",
    post_hook=self.save_draft,
)  

Call a Timbal Agent to generate the draft

Process incoming emails by creating the prompt and invoking the Agent to generate a response:
def _create_email_prompt(self, email):
    # Extract email information
    subject = email.get('subject', 'No Subject')
    sender = email.get('from', 'Unknown Sender')
    body = email.get('body', '')
    snippet = email.get('snippet', '')
    content = body if body else snippet
    
    prompt = f"""
You are an AI assistant that helps draft professional email responses. 

Please write a helpful and appropriate response to this email:

**Received Email:**
From: {sender}
Subject: {subject}
Content: {content}

**Instructions:**
- Write a professional response
- Do not assume anything that is not explicitly stated in the email
- Address any questions or requests appropriately
- Keep the tone friendly but business-appropriate
- If you need more information, ask clarifying questions
- End with a professional closing
- Keep the response concise but complete

**Your Response:**
"""
    return prompt.strip()


async def generate_draft(self, message):
    prompt = self._create_email_prompt(message)
    
    # Generate and create draft
    try:
        response_text = await self.agent(prompt=prompt, input_email=message).collect()
        if response_text:
            print("Generated Response:")
            print("-" * 40)
            print(response_text)
            print("-" * 40)
            
            print("Draft created successfully!")
            print("="*60)
        else:
            print("Failed to generate response")
    except Exception as e:
        print(f"Error generating/creating draft: {e}")
    
    print("="*60)

Save the draft to the emails thread

Define the post-hook function that automatically saves the AI-generated response as a draft reply in the original email thread:
def save_draft(self):
    """Create a draft response to the original message"""
    span = get_run_context().current_span()
    original_message = span.input['input_email']
    response_text = span.output.content[0].text

    try:
        # Extract sender email from the original message
        from_header = original_message.get('from', '')
        if '<' in from_header and '>' in from_header:
            # Extract email from "Name <email@domain.com>" format
            sender_email = from_header.split('<')[1].split('>')[0].strip()
        else:
            # Assume the entire from header is the email
            sender_email = from_header.strip()
        
        # Create response subject
        original_subject = original_message.get('subject', '')
        if not original_subject.startswith('Re:'):
            response_subject = f"Re: {original_subject}"
        else:
            response_subject = original_subject
        
        # Create draft message
        draft_message = self._create_message(sender_email, response_subject, response_text)
        
        # Create the draft with thread ID to link it to the original conversation
        draft_body = {
            'message': {
                'raw': draft_message
            }
        }
        
        # Include thread ID if available to link the draft to the original conversation
        if original_message.get('thread_id'):
            draft_body['message']['threadId'] = original_message['thread_id']
        
        draft = self.gmail_service.users().drafts().create(
            userId='me',
            body=draft_body
        ).execute()
        
        print(f"Draft ID: {draft['id']}")
        print(f"To: {sender_email}")
        print(f"Subject: {response_subject}")
        return draft
        
    except Exception as e:
        print(f"Error creating draft: {e}")
        return None

Script Key Features

  • Real-time Processing: Constantly polls new emails
  • Intelligent Responses: Uses AI to generate contextually appropriate draft replies
  • History Tracking: Maintains state to avoid processing duplicate emails or generated drafts

Next Steps

This example demonstrates a simple implementation to showcase Timbal’s capabilities for Gmail automation. For a more sophisticated solution, consider configuring Gmail to send notifications to a Pub/Sub topic when new messages arrive.

Troubleshooting

  • Missing scopes: Verify all required Gmail API scopes are included
  • OAuth issues: Check that credentials.json and token.json files exist and are valid
  • Service Account issues: Verify domain-wide delegation is configured and DELEGATED_USER has correct email
  • Rate limits: Gmail API has daily quotas. Check your usage.
  • Permission denied: For Service Accounts, ensure domain-wide delegation is authorized with correct Client ID
  • Import errors: Ensure all required packages are installed
  • File errors: Check that your credentials file exists and is properly formatted