Turn Google ADK into your Gmail and Calendar Assistant
In this hands-on guide, you’ll learn exactly how to integrate Gmail and Google Calendar into your ADK agent. Less theory, more practice—so…
Short yet Powerful Tutorial: Google ADK + Gmail + Calendar
Turn Google ADK into your Gmail and Calendar Assistant

In this hands-on guide, you’ll learn exactly how to integrate Gmail and Google Calendar into your ADK agent. Less theory, more practice—so you can get up and running fast.
Background
We just won the Grand Prize of the **Agent Development Kit Hackathon* with Google Cloud, where we, with Sergazy Nurbavliyev, built a feature-rich integration using Gmail and Calendar for an autonomous SDR system that discovers leads, researches prospects, and makes phone calls—all powered by Google’s Gemini 2.0 and modern AI agents.*
During my search, I couldn’t find a simple, quick-start guide—just scattered documentation and fragmented examples. So, I took the long road, absorbed the complexity, and figured it out step by step.
Now I’m sharing everything I wish I had at the start—a clear, beginner-friendly path to building this integration.
We’ll use Google Colab to make things easy and reproducible. I’ll provide the notebook at the end so you can follow along and try it yourself.
Let’s start with getting your credentials.
Getting the credentials from the Google Cloud Console

To obtain the required credentials, open Google Cloud Console, navigate to APIs & Services → Credentials, and first configure an OAuth consent screen with the basic information Google asks for (app name, support email, etc.) so that users can authorize your app properly.
Next, click Create Credentials → OAuth client ID, pick “Web application”, give the client a meaningful name, and—crucially—add at least one Authorized redirect URI (for local testing, a common choice is http://localhost:8000/callback) so Google knows where to send the user after they grant access. After saving, Google will display the Client ID and Client Secret; copy both of these (or download a JSON), along with the redirect URI you just registered, because you will need all three values in your code or environment variables whenever your agents initiate OAuth flows. Save the JSON or variables into you Colab secrets
Part 1: Setting Up the Foundation
Every project starts with the right tools. The first step is to set up the Python environment and install the necessary libraries.
!pip install google-adk -q
!pip install litellm -q
!pip install deprecated -q
!pip install typing-extensions -q
!pip install google-cloud-aiplatform -q
With the libraries installed, the next step is to import all the necessary modules into our script.
# @title Import necessary libraris
import os
import asyncio
from google.adk.agents import Agent
from google.adk.models.lite_llm import LiteLlm
from google.adk.sessions import InMemorySessionService
from google.adk.runners import Runner
from google.genai import types # For creating message Content/Parts
import warnings
# Ignore all warnings
warnings.filterwarnings("ignore")
import logging
logging.basicConfig(level=logging.ERROR)
print("Libraries imported.")
Part 2: Authentication—The Keys to the Kingdom
To allow our agent to access private user data, we need to handle authentication using an API key for Gemini and OAuth 2.0 credentials for Google services. In Google Colab, we can use the userdatafeature to securely store and access these secrets.
from google.colab import userdata
# --- IMPORTANT: Replace placeholders with your real API keys ---
# Make sure to add these secrets to your Colab environment
# (Click the key icon on the left sidebar)
try:
api_key = userdata.get('GEMINI_API_KEY')
os.environ["GEMINI_API_KEY"] = api_key
GOOGLE_CLOUD_CLIENT_ID = userdata.get('GOOGLE_CLOUD_CLIENT_ID')
GOOGLE_CLOUD_CLIENT_SECRET = userdata.get('GOOGLE_CLOUD_CLIENT_SECRET')
GEMINI_API_KEY = userdata.get('GEMINI_API_KEY')
MODEL = "gemini-1.5-flash"
# For Google Admin Workspace project (later)
CLOUD_PROJECT_ID="your-id"
CLOUD_PROJECT_REGION="us-central1"
print("Secrets loaded successfully.")
except Exception as e:
print("Could not load secrets. Please make sure you have added GEMINI_API_KEY, GOOGLE_CLOUD_CLIENT_ID, and GOOGLE_CLOUD_CLIENT_SECRET to your Colab secrets.")
or from the JSON we just downloaded
# prompt: import client_secret_762506172719-jkc8verldrb6lfcabkb42t65pr9l36q8 and set dredentials just for client id and secret from that json client_id = client_info.get(web).get('client_id')
import json
with open('/content/client_secret_76...9l36q8.apps.googleusercontent.com.json', 'r') as f:
client_info = json.load(f)
# Assuming your client secret file has a 'web' key with 'client_id' and 'client_secret'
client_id = client_info.get('web').get('client_id')
client_secret = client_info.get('web').get('client_secret')
print(client_id)
print(client_secret)
Part 3: Defining the Agent and Tools
Now we define the agent and give it tools. The ADK provides pre-built and makes GmailToolset and CalendarToolset integration simple.
from google.adk.auth import AuthConfig
from google.adk.agents import Agent
from google.adk.events import Event
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.tools.google_api_tool import GmailToolset, CalendarToolset
from google.genai import types
import asyncio
import base64
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import os # Import os for path handling
# --- Helper Functions ---
async def get_user_input(prompt: str) -> str:
"""
Asynchronously prompts the user for input in the console.
Uses asyncio's event loop and run_in_executor to avoid blocking the main
asynchronous execution thread while waiting for synchronous `input()`.
Args:
prompt: The message to display to the user.
Returns:
The string entered by the user.
"""
loop = asyncio.get_event_loop()
# Run the blocking `input()` function in a separate thread managed by the executor.
return await loop.run_in_executor(None, input, prompt)
def is_pending_auth_event(event: Event) -> bool:
"""
Checks if an ADK Event represents a request for user authentication credentials.
The ADK framework emits a specific function call ('adk_request_credential')
when a tool requires authentication that hasn't been previously satisfied.
Args:
event: The ADK Event object to inspect.
Returns:
True if the event is an 'adk_request_credential' function call, False otherwise.
"""
# Safely checks nested attributes to avoid errors if event structure is incomplete.
return (
event.content
and event.content.parts
and any(
part.function_call
and part.function_call.name == 'adk_request_credential'
for part in event.content.parts
)
)
def get_function_call_id(event: Event) -> str:
"""
Extracts the unique ID of the function call from an ADK Event.
This ID is crucial for correlating a function *response* back to the specific
function *call* that the agent initiated to request for auth credentials.
Args:
event: The ADK Event object containing the function call.
Returns:
The unique identifier string of the function call.
Raises:
ValueError: If the function call ID cannot be found in the event structure.
"""
if (
event
and event.content
and event.content.parts
):
for part in event.content.parts:
if part.function_call and part.function_call.id:
return part.function_call.id
# If the ID is missing, raise an error indicating an unexpected event format.
raise ValueError(f'Cannot get function call id from event {event}')
def get_function_call_auth_config(event: Event) -> AuthConfig:
"""
Extracts the authentication configuration details from an 'adk_request_credential' event.
Client should use this AuthConfig to necessary authentication details (like OAuth codes and state)
and sent it back to the ADK to continue OAuth token exchanging.
Args:
event: The ADK Event object containing the 'adk_request_credential' call.
Returns:
An AuthConfig object populated with details from the function call arguments.
Raises:
ValueError: If the 'auth_config' argument cannot be found in the event.
"""
if (
event
and event.content
and event.content.parts
):
for part in event.content.parts:
if (
part.function_call
and part.function_call.name == 'adk_request_credential'
and part.function_call.args
and part.function_call.args.get('authConfig')
):
return AuthConfig(
**part.function_call.args.get('authConfig')
)
raise ValueError(f'Cannot get auth config from event {event}')
# Helper function to create RFC822 formatted message
def create_rfc822_message(to_email: str, subject: str, body: str, from_email: str = None, file_paths: list = None):
"""
Creates an RFC822 formatted message string with optional attachments that Gmail API can use.
Args:
to_email: Recipient email address
subject: Email subject
body: Email body text
from_email: Sender email (optional, will use authenticated user's email if not provided)
file_paths: A list of file paths to attach (e.g., ['path/to/file1.pdf', 'path/to/image.png'])
Returns:
Base64 encoded RFC822 message string
"""
if file_paths:
# Create a multipart message for attachments
msg = MIMEMultipart()
else:
# Create a simple text message if no attachments
msg = MIMEText(body, 'plain')
msg['To'] = to_email
msg['Subject'] = subject
# Add From header if provided
if from_email:
msg['From'] = from_email
# Attach the email body if it's a multipart message
if file_paths:
msg.attach(MIMEText(body, 'plain'))
for file_path in file_paths:
try:
# Guess the MIME type of the file
import mimetypes
content_type, encoding = mimetypes.guess_type(file_path)
if content_type is None or encoding is not None:
content_type = 'application/octet-stream' # Default if type can't be guessed
main_type, sub_type = content_type.split('/', 1)
with open(file_path, 'rb') as f:
file_data = f.read()
if main_type == 'text':
part = MIMEText(file_data.decode('utf-8'), _subtype=sub_type)
else:
part = MIMEBase(main_type, sub_type)
part.set_payload(file_data)
encoders.encode_base64(part) # Encode content to base64
# Add header with the filename
filename = os.path.basename(file_path)
part.add_header('Content-Disposition', 'attachment', filename=filename)
msg.attach(part)
except Exception as e:
print(f"Error attaching file {file_path}: {e}")
# You might want to handle this error more gracefully, e.g., raise an exception
continue # Continue to the next file if one fails
# Convert to RFC822 format and encode
rfc822_message = msg.as_string()
# Gmail API expects the message to be base64url encoded
encoded_message = base64.urlsafe_b64encode(rfc822_message.encode('utf-8')).decode('utf-8')
return encoded_message
gmail_tool = GmailToolset(
client_id = GOOGLE_CLOUD_CLIENT_ID,
client_secret = GOOGLE_CLOUD_CLIENT_SECRET
)
gmail_tool.configure_auth(
client_id=GOOGLE_CLOUD_CLIENT_ID,
client_secret=GOOGLE_CLOUD_CLIENT_SECRET
)
calendar_tool = CalendarToolset(
client_id = GOOGLE_CLOUD_CLIENT_ID,
client_secret = GOOGLE_CLOUD_CLIENT_SECRET
)
calendar_tool.configure_auth(
client_id=GOOGLE_CLOUD_CLIENT_ID,
client_secret=GOOGLE_CLOUD_CLIENT_SECRET
)
gmail_agent = Agent(
name="gmail_agent_v1",
model=MODEL,
description="Have access to the Gmail and Calendar services and can send read emails and view, create evnets on calendar",
instruction="""
You are the Gmail assistant. You can use the 'gmail_tool' to access, read and send emails.
""",
tools=[gmail_tool, calendar_tool],
)
Part 4: Running the Agent and Handling Tasks
The Runner manages the interaction loop and a SessionService keeps track of the conversation history. The call_agent_with_auth_flow function sends a user's query to the agent and manages the authentication handshake.
# --- Session Management ---
session_service = InMemorySessionService()
APP_NAME = "google_toolset_hackathon_app"
USER_ID = "user_1"
SESSION_ID = "session_001"
session = await session_service.create_session(
app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID
)
print(f"Session created: App='{APP_NAME}', User='{USER_ID}', Session='{SESSION_ID}'")
# --- Runner ---
runner = Runner(agent=gmail_agent, app_name=APP_NAME, session_service=session_service)
print(f"Runner created for agent '{runner.agent.name}'.")
async def call_agent_with_auth_flow(query: str, runner, user_id, session_id):
"""Sends a query to the agent and handles the full authentication flow."""
print(f"\n>>> User Query: {query}")
content = types.Content(role='user', parts=[types.Part(text=query)])
auth_request_function_call_id, auth_config = None, None
final_response_text = "Agent did not produce a final response."
# --- First Run: Send initial query ---
print("\nRunning agent...")
async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=content):
print(f" [Event] Author: {event.author}, Final: {event.is_final_response()}, Content: {event.content}")
if is_pending_auth_event(event):
print("--> Authentication required by agent.")
auth_request_function_call_id = get_function_call_id(event)
auth_config = get_function_call_auth_config(event)
break
if event.is_final_response():
if event.content and event.content.parts:
final_response_text = event.content.parts[0].text
break
# --- Handle Authentication if required ---
if auth_request_function_call_id and auth_config:
# IMPORTANT: This must match a URI in your GCP OAuth Client settings
redirect_uri = 'http://localhost:8000/callback' # This won't work directly in Colab without a tunnel
if (auth_config.exchanged_auth_credential and
auth_config.exchanged_auth_credential.oauth2 and
auth_config.exchanged_auth_credential.oauth2.auth_uri):
base_auth_uri = auth_config.exchanged_auth_credential.oauth2.auth_uri
auth_request_uri = base_auth_uri + f'&redirect_uri={redirect_uri}'
print("\n--- User Action Required ---")
print(f'1. Please open this URL in your browser to authorize:\n {auth_request_uri}\n')
print('2. After authorizing, copy the *entire* URL from your browser\'s address bar.')
print(f'3. Paste the copied URL here and press Enter:\n')
auth_response_uri = await get_user_input('> ')
auth_config.exchanged_auth_credential.oauth2.auth_response_uri = auth_response_uri
auth_config.exchanged_auth_credential.oauth2.redirect_uri = redirect_uri
auth_content = types.Content(role='user', parts=[types.Part(
function_response=types.FunctionResponse(
id=auth_request_function_call_id,
name='adk_request_credential',
response=auth_config.model_dump(),
))])
# --- Second Run: Resume agent with auth details ---
print("\nSubmitting authentication details back to the agent...")
async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=auth_content):
print(f" [Event] Author: {event.author}, Final: {event.is_final_response()}, Content: {event.content}")
if event.is_final_response():
if event.content and event.content.parts:
final_response_text = event.content.parts[0].text
break
else:
final_response_text = "Error: Authentication URI not found."
print(f"\n<<< Agent Response: {final_response_text}")
return final_response_text
Example 1: Sending an Email
The gmail_users_messages_send tool requires the email content to be in a specific base64-encoded RFC822 format.
# --- Create the RFC822 formatted message ---
rfc822_message = create_rfc822_message(
to_email="your_email@example.com", # CHANGE THIS
subject="Sales ADK Hackathon Check-in",
body="Hi, this is a test email sent from my Google ADK agent. The integration is working!",
)
# --- Construct the query for the agent ---
email_query = f"""
Please send an email using the gmail_users_messages_send tool with the following parameters:
- userId: 'me'
- message: '{rfc822_message}'
This message is already properly formatted in RFC822 format and base64url encoded.
"""
# --- Run the agent ---
# Note: The auth flow will require manual steps.
# await call_agent_with_auth_flow(email_query, runner=runner, user_id=USER_ID, session_id=SESSION_ID)
print("Email query is ready. Uncomment the line above to run the agent.")
The result should be like that.



Copy the URL

Paste it,, and you are ready to receive the email.

Now you have got the idea, let’s continue.
Example 2: Reading Recent Emails
This demonstrates the agent’s ability to read emails.
# --- Query to read emails ---
read_email_query = "Read the subject of my 2 most recent unread emails."
# --- Run the agent ---
# await call_agent_with_auth_flow(read_email_query, runner=runner, user_id=USER_ID, session_id=SESSION_ID)
print("Read email query is ready. Uncomment the line above to run the agent.")
Example 3: Creating a Calendar Event
The agent parses a natural language request to create a calendar event.
# --- Query to create a calendar event ---
create_event_query = (
"Schedule a meeting with 'example.customer@email.com' for tomorrow at 3 PM "
"titled 'Project Follow-up'. Set the duration to 45 minutes and add a "
"description that says 'Discussing next steps for the project.'"
)
# --- Run the agent ---
# await call_agent_with_auth_flow(create_event_query, runner=runner, user_id=USER_ID, session_id=SESSION_ID)
print("Create event query is ready. Uncomment the line above to run the agent.")
Part 5: Alternative: Using a Service Account for Full Automation
Ok, but I want to send the email without human interaction, right?
But before that you need to spend some time to get things done first.
Create the Service Account in Google Cloud
First, you need to create a service account and its key.
Go to the Service Accounts page:
- Open the Google Cloud Console and navigate to the **Service Accounts** page.
- Select the project you are using for this integration.
Create the Service Account:
- Click + CREATE SERVICE ACCOUNT.
- Give it a Service account name (e.g., “sales-automation-agent”) and an optional description. The Service account ID will be generated automatically.
- Click CREATE AND CONTINUE.
Grant Access (Optional but Recommended):
- You can grant the service account roles on the project, but for this use case, the primary permissions will be granted via domain-wide delegation. You can skip adding project-level roles for now.
- Click CONTINUE.
Create a Key:
- In the “Grant users access to this service account” step, scroll down to the Keys section and click + CREATE KEY.
- Select JSON as the key type and click CREATE.
- A JSON file containing your service account’s private key will be downloaded to your computer. Treat this file like a password; it is highly sensitive. You will need to upload this file to your Colab environment (e.g., as
sales-automation-service.json).
Find the Client ID:
- After creating the key, you will be returned to the service accounts list. Click on the service account you just created.
- Go to the Details tab.
- Find and copy the Unique ID (this is a long number, e.g.,
112233445566778899000). This is the Client ID you will need for the next part.
Authorize the Service Account in Google Workspace (Domain-Wide Delegation)
Next, a Google Workspace administrator must authorize the service account to access user data.
Go to your Google Workspace Admin console:
- Open the Google Workspace Admin console.
- Navigate to Security > Access and data control > API controls.
Manage Domain-Wide Delegation:
- In the “API controls” section, click on MANAGE DOMAIN WIDE DELEGATION.
Add a New API Client:
- Click Add new.
- In the Client ID field, paste the Unique ID of the service account you copied from the Google Cloud Console in the previous section.
- In the OAuth scopes (comma-delimited) field, you must enter the specific API scopes your application needs. For the examples in Canvas, you would need:
https://www.googleapis.com/auth/gmail.send,https://www.googleapis.com/auth/gmail.readonly,https://www.googleapis.com/auth/gmail.modify,https://www.googleapis.com/auth/calendar,https://www.googleapis.com/auth/calendar.events
Authorize:
- Click Authorize. The client will now appear in the list, and your service account is ready to make API calls on behalf of users in your domain.
Setting up the Environment for Service Accounts
A service account is a non-human user that can be authorized to access Google Workspace data on behalf of users in your organization without manual interaction.
!pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client -q
print("Service Account libraries installed.")
Before you begin, check the domain-wide delegation
import json
from google.oauth2 import service_account
from google.auth.transport.requests import Request
def test_delegation():
SERVICE_ACCOUNT_FILE = 'sales-automation-service.json'
SALES_EMAIL = 'sales@zemzen.org'
SCOPES = [
'https://www.googleapis.com/auth/gmail.send',
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.modify'
]
try:
# Load service account info
with open(SERVICE_ACCOUNT_FILE, 'r') as f:
sa_info = json.load(f)
print("📋 Service Account Information:")
print(f" Client ID: {sa_info['client_id']}")
print(f" Client Email: {sa_info['client_email']}")
print(f" Project ID: {sa_info['project_id']}")
print(f" Target User: {SALES_EMAIL}")
print(f"\n🔑 COPY THIS CLIENT ID TO GOOGLE ADMIN CONSOLE:")
print(f" {sa_info['client_id']}")
print(f"\n🔗 COPY THESE SCOPES TO GOOGLE ADMIN CONSOLE:")
print(f" https://www.googleapis.com/auth/gmail.send,https://www.googleapis.com/auth/gmail.readonly,https://www.googleapis.com/auth/gmail.modify")
# Test domain-wide delegation
print(f"\n🧪 Testing domain-wide delegation...")
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
delegated_creds = credentials.with_subject(SALES_EMAIL)
delegated_creds.refresh(Request())
print("✅ SUCCESS! Domain-wide delegation is working!")
print("🎉 You can now send emails!")
return True
except Exception as e:
error_str = str(e)
print(f"\n❌ FAILED: {error_str}")
if 'invalid_scope' in error_str:
print("\n🚨 DOMAIN-WIDE DELEGATION NOT CONFIGURED!")
print("📝 Go to admin.google.com and configure it with the info above")
return False
# Run the test
test_delegation()
Example 1: Sending an Email with a Service Account
This script uses a service account with delegated credentials to send an email. You will need to create a service account in your Google Cloud Project, grant it domain-wide delegation, and download the JSON key file.
You will need to upload your service account JSON file and a sample PDF to your Colab environment for this to work.
import json
import base64
import os
import mimetypes
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
from google.oauth2 import service_account
from googleapiclient.discovery import build
def send_email_from_sales_with_attachment():
"""Send email from sales@zemzen.org to meinnps@gmail.com with PDF attachment"""
# Configuration
SERVICE_ACCOUNT_FILE = 'sales-automation-service.json'
SALES_EMAIL = 'sales@zemzen.org'
TARGET_EMAIL = 'meinnps@gmail.com'
ATTACHMENT_PATH = '/content/SalesShortcut_Proposal.pdf' # Your PDF attachment
SCOPES = [
'https://www.googleapis.com/auth/gmail.send',
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.modify'
]
try:
print("📧 Preparing to send email with attachment...")
print(f" From: {SALES_EMAIL}")
print(f" To: {TARGET_EMAIL}")
print(f" Attachment: {ATTACHMENT_PATH}")
# Check if attachment exists
if not os.path.exists(ATTACHMENT_PATH):
print(f"⚠️ Warning: Attachment file not found at {ATTACHMENT_PATH}")
print(" Sending email without attachment...")
ATTACHMENT_PATH = None
# Create credentials
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
delegated_creds = credentials.with_subject(SALES_EMAIL)
# Create Gmail service
service = build('gmail', 'v1', credentials=delegated_creds)
# Create email message
subject = "Sales Proposal - SalesShortcut Solution 🚀"
body = """
Hello!
We're excited to share our SalesShortcut proposal with you. Please find the detailed proposal document attached.
📎 **Attached Document:** SalesShortcut_Proposal.pdf
**Key highlights:**
✅ Automated email campaigns
✅ Advanced customer tracking
✅ Seamless integration capabilities
✅ Comprehensive analytics dashboard
This proposal outlines how SalesShortcut can streamline your sales processes and boost your team's productivity.
We'd love to schedule a call to discuss this proposal in detail and answer any questions you might have.
Best regards,
**Sales Team**
Zemzen Organization
📧 sales@zemzen.org
📞 Contact us for a demo!
P.S. - This email was sent using our automated system, demonstrating one of the many capabilities we can implement for your business!
"""
# Create multipart message for attachment
if ATTACHMENT_PATH and os.path.exists(ATTACHMENT_PATH):
message = MIMEMultipart()
message.attach(MIMEText(body, 'plain'))
# Add attachment
print("📎 Adding PDF attachment...")
# Determine MIME type
content_type, encoding = mimetypes.guess_type(ATTACHMENT_PATH)
if content_type is None or encoding is not None:
content_type = 'application/pdf' # Default for PDF
main_type, sub_type = content_type.split('/', 1)
# Read the file
with open(ATTACHMENT_PATH, 'rb') as attachment_file:
file_data = attachment_file.read()
# Create attachment part
attachment_part = MIMEBase(main_type, sub_type)
attachment_part.set_payload(file_data)
encoders.encode_base64(attachment_part)
# Add header with filename
filename = os.path.basename(ATTACHMENT_PATH)
attachment_part.add_header(
'Content-Disposition',
f'attachment; filename= {filename}'
)
# Attach to message
message.attach(attachment_part)
print(f"✅ Attachment added: {filename}")
else:
# Simple text message if no attachment
message = MIMEText(body, 'plain')
# Set email headers
message['to'] = TARGET_EMAIL
message['from'] = SALES_EMAIL
message['subject'] = subject
# Send email
print("📤 Sending email...")
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
result = service.users().messages().send(
userId='me',
body={'raw': raw_message}
).execute()
print("✅ EMAIL SENT SUCCESSFULLY!")
print(f" Message ID: {result.get('id')}")
print(f" Thread ID: {result.get('threadId')}")
if ATTACHMENT_PATH and os.path.exists(ATTACHMENT_PATH):
print(f" 📎 Attachment: {os.path.basename(ATTACHMENT_PATH)} included")
print(f" 📬 Check {TARGET_EMAIL} inbox!")
return result
except Exception as e:
print(f"❌ Error sending email: {e}")
return None
# Send the email with attachment
print("🚀 Sending Sales Proposal Email with PDF Attachment")
print("=" * 60)
send_email_from_sales_with_attachment()Example 2: Checking for Unread Emails with a Service Account
Example 2: Checking for Unread Emails with a Service Account
This script can run in the background to check an inbox for unread messages.
import json
import base64
from google.oauth2 import service_account
from googleapiclient.discovery import build
from datetime import datetime
import html
def check_unread_emails():
"""Check unread emails and access all threads for sales@zemzen.org"""
# Configuration
SERVICE_ACCOUNT_FILE = 'sales-automation-service.json'
SALES_EMAIL = 'sales@zemzen.org'
SCOPES = [
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.modify'
]
try:
print("📬 Checking unread emails...")
print(f" Account: {SALES_EMAIL}")
print("=" * 60)
# Create credentials with delegation
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
delegated_creds = credentials.with_subject(SALES_EMAIL)
# Create Gmail service
service = build('gmail', 'v1', credentials=delegated_creds)
# Get unread messages
results = service.users().messages().list(
userId='me',
q='is:unread',
maxResults=50
).execute()
messages = results.get('messages', [])
if not messages:
print("✅ No unread emails found!")
return []
print(f"📧 Found {len(messages)} unread email(s)")
print("=" * 60)
unread_emails = []
for i, message in enumerate(messages, 1):
message_id = message['id']
# Get detailed message info
msg = service.users().messages().get(
userId='me',
id=message_id,
format='full'
).execute()
# Extract headers
headers = msg['payload'].get('headers', [])
subject = next((h['value'] for h in headers if h['name'] == 'Subject'), 'No Subject')
sender = next((h['value'] for h in headers if h['name'] == 'From'), 'Unknown Sender')
date = next((h['value'] for h in headers if h['name'] == 'Date'), 'No Date')
thread_id = msg.get('threadId')
# Get message body
body = extract_message_body(msg)
# Get thread details
thread_info = get_thread_details(service, thread_id)
email_data = {
'message_id': message_id,
'thread_id': thread_id,
'subject': subject,
'sender': sender,
'date': date,
'body': body[:200] + '...' if len(body) > 200 else body,
'full_body': body,
'thread_message_count': thread_info['message_count'],
'thread_participants': thread_info['participants']
}
unread_emails.append(email_data)
print(f"📧 Email #{i}")
print(f" 📨 From: {sender}")
print(f" 📋 Subject: {subject}")
print(f" 📅 Date: {date}")
print(f" 🧵 Thread ID: {thread_id}")
print(f" 📊 Thread Messages: {thread_info['message_count']}")
print(f" 👥 Thread Participants: {', '.join(thread_info['participants'])}")
print(f" 📄 Preview: {body[:150]}{'...' if len(body) > 150 else ''}")
print("-" * 60)
return unread_emails
except Exception as e:
print(f"❌ Error checking emails: {e}")
return []
def extract_message_body(message):
"""Extract text body from Gmail message"""
body = ""
def extract_text_from_part(part):
if part.get('mimeType') == 'text/plain':
data = part.get('body', {}).get('data')
if data:
return base64.urlsafe_b64decode(data).decode('utf-8')
elif part.get('mimeType') == 'text/html':
data = part.get('body', {}).get('data')
if data:
html_content = base64.urlsafe_b64decode(data).decode('utf-8')
# Simple HTML to text conversion (remove tags)
return html.unescape(html_content)
return ""
payload = message.get('payload', {})
# Single part message
if payload.get('body', {}).get('data'):
body = extract_text_from_part(payload)
# Multi-part message
elif payload.get('parts'):
for part in payload['parts']:
if part.get('parts'): # Nested parts
for nested_part in part['parts']:
text = extract_text_from_part(nested_part)
if text:
body += text + "\n"
else:
text = extract_text_from_part(part)
if text:
body += text + "\n"
return body.strip()
def get_thread_details(service, thread_id):
"""Get detailed information about an email thread"""
try:
thread = service.users().threads().get(
userId='me',
id=thread_id
).execute()
messages = thread.get('messages', [])
participants = set()
for msg in messages:
headers = msg['payload'].get('headers', [])
from_header = next((h['value'] for h in headers if h['name'] == 'From'), '')
to_header = next((h['value'] for h in headers if h['name'] == 'To'), '')
# Extract email addresses
if from_header:
participants.add(extract_email_address(from_header))
if to_header:
for email in to_header.split(','):
participants.add(extract_email_address(email.strip()))
return {
'message_count': len(messages),
'participants': list(participants)
}
except Exception as e:
print(f"⚠️ Error getting thread details: {e}")
return {'message_count': 1, 'participants': ['Unknown']}
def extract_email_address(email_string):
"""Extract email address from 'Name <email@domain.com>' format"""
if '<' in email_string and '>' in email_string:
return email_string.split('<')[1].split('>')[0]
return email_string.strip()
def mark_as_read(message_ids):
"""Mark specific messages as read"""
SERVICE_ACCOUNT_FILE = 'sales-automation-service.json'
SALES_EMAIL = 'sales@zemzen.org'
SCOPES = [
'https://www.googleapis.com/auth/gmail.modify'
]
try:
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
delegated_creds = credentials.with_subject(SALES_EMAIL)
service = build('gmail', 'v1', credentials=delegated_creds)
for message_id in message_ids:
service.users().messages().modify(
userId='me',
id=message_id,
body={'removeLabelIds': ['UNREAD']}
).execute()
print(f"✅ Marked {len(message_ids)} message(s) as read")
except Exception as e:
print(f"❌ Error marking messages as read: {e}")
def get_thread_conversation(thread_id):
"""Get full conversation history for a specific thread"""
SERVICE_ACCOUNT_FILE = 'sales-automation-service.json'
SALES_EMAIL = 'sales@zemzen.org'
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']
try:
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
delegated_creds = credentials.with_subject(SALES_EMAIL)
service = build('gmail', 'v1', credentials=delegated_creds)
thread = service.users().threads().get(
userId='me',
id=thread_id
).execute()
messages = thread.get('messages', [])
conversation = []
print(f"🧵 Thread Conversation ({len(messages)} messages)")
print("=" * 60)
for i, msg in enumerate(messages, 1):
headers = msg['payload'].get('headers', [])
subject = next((h['value'] for h in headers if h['name'] == 'Subject'), 'No Subject')
sender = next((h['value'] for h in headers if h['name'] == 'From'), 'Unknown')
date = next((h['value'] for h in headers if h['name'] == 'Date'), 'No Date')
body = extract_message_body(msg)
message_data = {
'sequence': i,
'message_id': msg['id'],
'subject': subject,
'sender': sender,
'date': date,
'body': body
}
conversation.append(message_data)
print(f"📧 Message #{i}")
print(f" From: {sender}")
print(f" Date: {date}")
print(f" Subject: {subject}")
print(f" Body: {body[:200]}{'...' if len(body) > 200 else ''}")
print("-" * 60)
return conversation
except Exception as e:
print(f"❌ Error getting thread conversation: {e}")
return []
# Main execution
if __name__ == "__main__":
print("🚀 Gmail Inbox Manager")
print("=" * 60)
# Check unread emails
unread_emails = check_unread_emails()
if unread_emails:
print(f"\n📊 Summary: Found {len(unread_emails)} unread emails")
# Example: Get full conversation for first thread
if unread_emails:
first_thread_id = unread_emails[0]['thread_id']
print(f"\n🔍 Getting full conversation for thread: {first_thread_id}")
get_thread_conversation(first_thread_id)Example 3: Managing the Calendar with a Service Account
Example 3: Managing the Calendar with a Service Account
This script automates calendar management, such as checking availability and creating events.
import json
import uuid # Added for Google Meet request IDs
from datetime import datetime, timedelta
from google.oauth2 import service_account
from googleapiclient.discovery import build
import pytz
def check_calendar_availability():
"""Check available dates and times for sales@zemzen.org calendar"""
# Configuration
SERVICE_ACCOUNT_FILE = 'sales-automation-service.json'
SALES_EMAIL = 'sales@zemzen.org'
SCOPES = [
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.events',
'https://www.googleapis.com/auth/calendar.readonly'
]
try:
print("📅 Checking calendar availability...")
print(f" Account: {SALES_EMAIL}")
print("=" * 60)
# Create credentials with delegation
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
delegated_creds = credentials.with_subject(SALES_EMAIL)
# Create Calendar service
service = build('calendar', 'v3', credentials=delegated_creds)
# Get timezone
calendar_info = service.calendars().get(calendarId='primary').execute()
timezone = calendar_info.get('timeZone', 'UTC')
tz = pytz.timezone(timezone)
print(f"🌍 Calendar timezone: {timezone}")
# Check availability for next 7 days
now = datetime.now(tz)
week_later = now + timedelta(days=7)
print(f"📊 Checking availability from {now.date()} to {week_later.date()}")
print("-" * 60)
# Get events for the next week
events_result = service.events().list(
calendarId='primary',
timeMin=now.isoformat(),
timeMax=week_later.isoformat(),
singleEvents=True,
orderBy='startTime'
).execute()
events = events_result.get('items', [])
if not events:
print("✅ No events found - completely available!")
available_slots = generate_available_slots(now, week_later, [])
return {
'existing_events': [],
'busy_slots': [],
'available_slots': available_slots,
'timezone': timezone
}
print(f"📅 Found {len(events)} existing events:")
busy_slots = []
for event in events:
start = event['start'].get('dateTime', event['start'].get('date'))
end = event['end'].get('dateTime', event['end'].get('date'))
summary = event.get('summary', 'No Title')
# Parse datetime
if 'T' in start: # Has time
start_dt = datetime.fromisoformat(start.replace('Z', '+00:00'))
end_dt = datetime.fromisoformat(end.replace('Z', '+00:00'))
else: # All-day event
start_dt = datetime.strptime(start, '%Y-%m-%d')
end_dt = datetime.strptime(end, '%Y-%m-%d')
busy_slots.append({
'start': start_dt,
'end': end_dt,
'summary': summary
})
print(f" 🗓️ {summary}")
print(f" 📅 {start_dt.strftime('%Y-%m-%d %H:%M')} - {end_dt.strftime('%Y-%m-%d %H:%M')}")
print("\n🟢 Available time slots:")
available_slots = generate_available_slots(now, week_later, busy_slots)
return {
'existing_events': events,
'busy_slots': busy_slots,
'available_slots': available_slots,
'timezone': timezone
}
except Exception as e:
print(f"❌ Error checking calendar: {e}")
return None
def generate_available_slots(start_date, end_date, busy_slots, slot_duration=60):
"""Generate available time slots between busy periods"""
# Business hours: 9 AM to 6 PM
business_start = 9
business_end = 18
available_slots = []
current_date = start_date.date()
end_date_only = end_date.date()
while current_date <= end_date_only:
# Skip weekends
if current_date.weekday() >= 5: # Saturday = 5, Sunday = 6
current_date += timedelta(days=1)
continue
# Create datetime objects for business hours
day_start = datetime.combine(current_date, datetime.min.time().replace(hour=business_start))
day_end = datetime.combine(current_date, datetime.min.time().replace(hour=business_end))
# Add timezone info
if hasattr(start_date, 'tzinfo') and start_date.tzinfo:
day_start = day_start.replace(tzinfo=start_date.tzinfo)
day_end = day_end.replace(tzinfo=start_date.tzinfo)
# Find busy slots for this day
day_busy_slots = []
for slot in busy_slots:
slot_start = slot['start']
slot_end = slot['end']
# Convert to same timezone if needed
if hasattr(day_start, 'tzinfo') and day_start.tzinfo:
if not hasattr(slot_start, 'tzinfo') or slot_start.tzinfo is None:
slot_start = slot_start.replace(tzinfo=day_start.tzinfo)
if not hasattr(slot_end, 'tzinfo') or slot_end.tzinfo is None:
slot_end = slot_end.replace(tzinfo=day_start.tzinfo)
# Check if busy slot overlaps with this day
if (slot_start.date() == current_date or
slot_end.date() == current_date or
(slot_start.date() < current_date < slot_end.date())):
# Adjust to business hours
overlap_start = max(day_start, slot_start)
overlap_end = min(day_end, slot_end)
if overlap_start < overlap_end:
day_busy_slots.append({
'start': overlap_start,
'end': overlap_end,
'summary': slot['summary']
})
# Sort busy slots by start time
day_busy_slots.sort(key=lambda x: x['start'])
# Find available slots
current_time = day_start
for busy_slot in day_busy_slots:
# Check if there's time before this busy slot
if current_time + timedelta(minutes=slot_duration) <= busy_slot['start']:
slot_end = busy_slot['start']
# Create available slots
while current_time + timedelta(minutes=slot_duration) <= slot_end:
available_slots.append({
'start': current_time,
'end': current_time + timedelta(minutes=slot_duration),
'date': current_date.strftime('%Y-%m-%d'),
'time': current_time.strftime('%H:%M'),
'duration_minutes': slot_duration
})
current_time += timedelta(minutes=slot_duration)
# Move past this busy slot
current_time = max(current_time, busy_slot['end'])
# Check for available time after last busy slot
while current_time + timedelta(minutes=slot_duration) <= day_end:
available_slots.append({
'start': current_time,
'end': current_time + timedelta(minutes=slot_duration),
'date': current_date.strftime('%Y-%m-%d'),
'time': current_time.strftime('%H:%M'),
'duration_minutes': slot_duration
})
current_time += timedelta(minutes=slot_duration)
# Move to next day
current_date += timedelta(days=1)
# Display available slots
if available_slots:
print("=" * 60)
current_display_date = None
for slot in available_slots[:20]: # Show first 20 slots
if slot['date'] != current_display_date:
current_display_date = slot['date']
print(f"\n📅 {current_display_date}:")
print(f" ⏰ {slot['time']} - {slot['end'].strftime('%H:%M')} ({slot['duration_minutes']} min)")
if len(available_slots) > 20:
print(f"\n... and {len(available_slots) - 20} more available slots")
else:
print("❌ No available slots found in the specified period")
return available_slots
def create_calendar_event(title, description, start_datetime, end_datetime, attendees=None):
"""Create a new calendar event with Google Meet"""
SERVICE_ACCOUNT_FILE = 'sales-automation-service.json'
SALES_EMAIL = 'sales@zemzen.org'
SCOPES = [
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.events'
]
try:
print("📅 Creating calendar event...")
print(f" Title: {title}")
print(f" Start: {start_datetime}")
print(f" End: {end_datetime}")
# Create credentials with delegation
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
delegated_creds = credentials.with_subject(SALES_EMAIL)
# Create Calendar service
service = build('calendar', 'v3', credentials=delegated_creds)
# Prepare event data
event_data = {
'summary': title,
'description': description,
'start': {
'dateTime': start_datetime.isoformat(),
'timeZone': 'America/New_York', # Adjust as needed
},
'end': {
'dateTime': end_datetime.isoformat(),
'timeZone': 'America/New_York', # Adjust as needed
},
'conferenceData': {
'createRequest': {
'requestId': f"{uuid.uuid4().hex}",
'conferenceSolutionKey': {
'type': 'hangoutsMeet'
}
}
},
'reminders': {
'useDefault': False,
'overrides': [
{'method': 'email', 'minutes': 24 * 60},
{'method': 'popup', 'minutes': 30},
],
},
}
if attendees:
event_data['attendees'] = [{'email': email} for email in attendees]
print(f" Attendees: {', '.join(attendees)}")
# --- KEY CHANGE IS HERE ---
# Add conferenceDataVersion=1 to the insert() call
event = service.events().insert(
calendarId='primary',
body=event_data,
sendUpdates='all' if attendees else 'none',
conferenceDataVersion=1 # <--- THIS LINE IS REQUIRED
).execute()
print("✅ EVENT CREATED SUCCESSFULLY!")
print(f" Event ID: {event.get('id')}")
print(f" Event Link: {event.get('htmlLink')}")
# --- ADD THIS TO PRINT THE MEET LINK ---
if 'conferenceData' in event:
meet_link = event['conferenceData']['entryPoints'][0]['uri']
print(f" 🎥 Meet Link: {meet_link}")
# ------------------------------------
print(f" 📧 {'Invitations sent to attendees' if attendees else 'No invitations sent'}")
return event
except Exception as e:
print(f"❌ Error creating event: {e}")
return None
def create_meeting_from_available_slot(slot_index, title, description, attendees=None):
"""Create a meeting using an available slot with Google Meet"""
# First check availability
availability = check_calendar_availability()
if not availability or not availability['available_slots']:
print("❌ No available slots found")
return None
available_slots = availability['available_slots']
if slot_index >= len(available_slots):
print(f"❌ Invalid slot index. Available slots: 0-{len(available_slots)-1}")
return None
slot = available_slots[slot_index]
print(f"\n🎯 Creating meeting in slot #{slot_index}")
print(f" 📅 {slot['date']} at {slot['time']}")
return create_calendar_event(
title=title,
description=description,
start_datetime=slot['start'],
end_datetime=slot['end'],
attendees=attendees
)
def get_upcoming_events(days_ahead=7):
"""Get upcoming events for the next N days"""
SERVICE_ACCOUNT_FILE = 'sales-automation-service.json'
SALES_EMAIL = 'sales@zemzen.org'
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
try:
print(f"📅 Getting upcoming events (next {days_ahead} days)...")
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
delegated_creds = credentials.with_subject(SALES_EMAIL)
service = build('calendar', 'v3', credentials=delegated_creds)
# Time range
now = datetime.utcnow()
future = now + timedelta(days=days_ahead)
events_result = service.events().list(
calendarId='primary',
timeMin=now.isoformat() + 'Z',
timeMax=future.isoformat() + 'Z',
singleEvents=True,
orderBy='startTime'
).execute()
events = events_result.get('items', [])
if not events:
print("✅ No upcoming events found")
return []
print(f"📊 Found {len(events)} upcoming events:")
print("=" * 60)
for i, event in enumerate(events, 1):
start = event['start'].get('dateTime', event['start'].get('date'))
summary = event.get('summary', 'No Title')
if 'T' in start:
start_dt = datetime.fromisoformat(start.replace('Z', '+00:00'))
print(f"📅 {i}. {summary}")
print(f" 📅 {start_dt.strftime('%Y-%m-%d %H:%M')}")
else:
print(f"📅 {i}. {summary} (All Day)")
print(f" 📅 {start}")
if event.get('description'):
desc = event['description'][:100] + '...' if len(event['description']) > 100 else event['description']
print(f" 📝 {desc}")
print("-" * 40)
return events
except Exception as e:
print(f"❌ Error getting upcoming events: {e}")
return []
def create_quick_meeting(title, date_str, time_str, duration_minutes=60, attendees=None):
"""Create a meeting for a specific date and time with Google Meet (easier to use)"""
try:
from datetime import datetime
import pytz
# Parse the date and time
meeting_datetime = datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")
# Add timezone (match your calendar timezone)
tz = pytz.timezone('America/Denver') # Match your calendar timezone
meeting_start = tz.localize(meeting_datetime)
meeting_end = meeting_start + timedelta(minutes=duration_minutes)
print(f"🎯 Creating quick meeting:")
print(f" 📅 {date_str} at {time_str}")
print(f" ⏱️ Duration: {duration_minutes} minutes")
print(f" 🎥 Google Meet: {'Included'}")
# Enhanced description for sales meetings
description = f"""Meeting scheduled for {duration_minutes} minutes.
📋 Agenda:
• Introduction and overview
• Product demonstration
• Q&A session
• Next steps discussion
🏢 Organized by: Zemzen Sales Team
📧 Contact: sales@zemzen.org
We look forward to speaking with you!"""
return create_calendar_event(
title=title,
description=description,
start_datetime=meeting_start,
end_datetime=meeting_end,
attendees=attendees,
)
except ValueError as e:
print(f"❌ Invalid date/time format. Use YYYY-MM-DD for date and HH:MM for time")
print(f" Example: create_quick_meeting('Demo Call', '2025-06-20', '14:00')")
return None
except Exception as e:
print(f"❌ Error creating quick meeting: {e}")
return None
if __name__ == "__main__":
print("🚀 Google Calendar Manager")
print("=" * 60)
# Check availability
availability = check_calendar_availability()
if availability and availability['available_slots']:
print(f"\n📊 Found {len(availability['available_slots'])} available slots")
# Test Google Meet functionality with a simple event
print("\n🧪 Testing Google Meet Integration:")
print("-" * 40)
from datetime import datetime, timedelta
import pytz
# Create a test meeting for 1 hour from now
tz = pytz.timezone('America/Denver') # Match your calendar timezone
now = datetime.now(tz)
start_time = now + timedelta(minutes=5)
end_time = start_time + timedelta(minutes=35)
print(f"🕐 Scheduling test meeting for: {start_time.strftime('%Y-%m-%d %H:%M %Z')}")
# Create a simple test event
result = create_calendar_event(
title="🧪 Test Google Meet Call",
description="This is a test meeting to verify Google Meet integration works correctly.\n\n✅ Testing automated calendar and video call creation.",
start_datetime=start_time,
end_datetime=end_time,
attendees=["meinnps@gmail.com"]
)
if result:
print("\n✅ Test completed successfully!")
print(" Check your calendar for the test meeting with Google Meet link")
else:
print("\n❌ Test failed - please check the error messages above")
# Get upcoming events
print("\n" + "=" * 60)
get_upcoming_events()
Links for the project and the original Hackathon submission
- Colab notebook
- Video demo of our project
- GitHub repo of our project
- Submission on Devpost
adkhackathon, #GoogleADK, #A2A, #CloudRun
메타데이터
- post_id
- 8fee1b1cb05f
- slug
- turn-google-adk-into-your-gmail-and-calendar-assistant-8fee1b1cb05f
- url
- https://medium.com/@meinnps/turn-google-adk-into-your-gmail-and-calendar-assistant-8fee1b1cb05f
- canonical_url
- https://medium.com/@meinnps/turn-google-adk-into-your-gmail-and-calendar-assistant-8fee1b1cb05f
- author_url
- https://medium.com/@meinnps
- status
- ok
- fetched_at
- 2026-07-18 11:23:00