← Back to list

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…

Merdan Durdyyev · 2025-06-23 21:31 · 4 claps · 22.3 min read
#adkhackathon #google-a2a #google-adk #google-gmail #google-calendar
Open on Medium ↗
Wiki topics: AGT · AI Agents

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:

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

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