Build an AI Email Assistant
Connect Gmail and Google Calendar to an AI that classifies your emails.
Introduction
30 Second Summary
Your inbox is overwhelming. Every morning, you scroll through dozens of emails trying to figure out which ones actually need your attention and which ones can wait.
In this project, you will build a Python command-line tool that connects to your real Gmail inbox. The tool uses AI to classify and prioritize your emails, then pushes important items straight to your Google Calendar.
What You'll Build
A Python email assistant that reads your Gmail, classifies emails with Gemini AI, and schedules important ones on your Google Calendar.
By the end of this project, you'll have:
- A Google Cloud project with OAuth 2.0 credentials that let your Python script access Gmail and Google Calendar.
- An AI-powered email classifier that categorizes your emails into urgent, opportunities, informational, and low priority using Gemini 2.5 Flash.
- A calendar integration that converts your priority emails into scheduled Google Calendar events.
- Secret Mission: VIP sender priority boosting that automatically ranks emails from important contacts higher.
Are there any prerequisites?
You'll need a Google account with Gmail, Python 3.10+ installed on your computer, and a Claude Code subscription. No prior experience with APIs or Google Cloud is required.
Set Up Your Google Cloud Project and API Keys
Your email assistant needs permission to access three Google services. It reads emails through the Gmail API, creates calendar events through the Google Calendar API, and classifies messages using Gemini AI.
Before writing any code, you need to set up the credentials and API keys that make these connections possible. This step handles all the configuration so you can focus on building in the next steps.
In this step, get ready to:
- Create a Google Cloud project and enable the Gmail and Calendar APIs.
- Configure OAuth 2.0 authentication and download your credentials file.
- Get a Gemini API key and install the required Python packages.
Create a Google Cloud project and enable APIs
The Google Cloud Console is the control center for managing which Google services your code can access. You need a project in the console before you can enable any APIs.
- Open your browser and go to the Google Cloud Console.
- Sign in with your Google account if prompted.
- Click the project dropdown at the top of the page (it may say Select a project).
- Click New Project.
- Enter Email Assistant in the Project name field.
- Click Create.
- Wait a few seconds for the project to be created, then select it from the project dropdown so it becomes your active project.
What is a Google Cloud project?
A Google Cloud project is a container that holds all your API credentials, usage quotas, and permissions. Every Google API you call needs to belong to a project so Google can track usage and enforce access controls.
Now enable the two Google APIs your assistant needs.
- In the left sidebar, click APIs & Services.
- Click Library.
- Search for Gmail API in the search bar.
- Click Gmail API from the results.
- Click Enable.
- Click the back arrow to return to the API Library.
- Search for Google Calendar API.
- Click Google Calendar API from the results.
- Click Enable.
You should now see both APIs listed under Enabled APIs & services in the left sidebar.
Don't see the Enable button?
Make sure you selected your Email Assistant project from the dropdown at the top of the page. APIs are enabled per project, so the wrong project means the button won't appear where you expect.
help me find and enable the Gmail API in Google Cloud Console
Configure OAuth and download credentials
Your script needs to prove it has your permission to read Gmail and create calendar events. OAuth 2.0 handles this by opening a browser window where you sign in and approve access.
First, create a project folder to store the credentials file you are about to download.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop and create the project folder by running these commands:
cd ~/Desktop
mkdir email-assistant
cd email-assistant
- Confirm the folder was created by checking your Desktop. You should see an email-assistant folder there.
Now head back to Google Cloud Console to set up the OAuth consent screen.
- In the left sidebar, click APIs & Services.
- Click OAuth consent screen.
- Select External as the user type.
- Click Create.
- Enter Email Assistant in the App name field.
- Select your email address in the User support email dropdown.
- Scroll down to Developer contact information and enter your email address.
- Click Save and Continue.
- On the Scopes page, click Save and Continue without adding anything.
- On the Test users page, click + Add Users.
- Enter your Gmail address and click Add.
- Click Save and Continue.
What is OAuth 2.0?
OAuth 2.0 is a standard that lets your script access your Google account without ever knowing your password. Instead, Google handles the sign-in directly and gives your script a temporary token to prove you approved access.
Choosing "External" means the app can be used by any Google account, not just ones on a company workspace. Your app stays in "Testing" mode, which is perfectly fine for personal use.
Now create the OAuth credentials that your Python script will use to start the sign-in flow.
- In the left sidebar under APIs & Services, click Credentials.
- Click + Create Credentials at the top of the page.
- Select OAuth client ID.
- Choose Desktop app as the Application type.
- Leave the default name or enter Email Assistant.
- Click Create.
A dialog pops up showing your client ID and client secret. You need to download the credentials file from this dialog.
- Click Download JSON.
- Find the downloaded file in your Downloads folder. It has a long name starting with client_secret.
- Move this file into the email-assistant folder on your Desktop.
- Rename the file to credentials.json.
What does credentials.json do?
This file contains your OAuth client ID and secret. Your Python script uses it to start the Google sign-in flow. It does not contain your Google password or personal data.
Can't find the downloaded file?
Check your browser's download bar at the bottom of the window. If the dialog already closed, go to Credentials in the left sidebar, find your OAuth client in the list, and click the download icon on the right side of its row.
help me download my OAuth credentials from Google Cloud Console
Get your Gemini API key and install Python packages
Your email assistant uses Gemini 2.5 Flash to classify and prioritize emails. To call the Gemini API, you need an API key from Google AI Studio.
- Open Google AI Studio in a new browser tab.
- Click Get API Key in the left sidebar.
- Click Create API Key.
- Select Create API key in new project.
- Copy the API key and save it somewhere safe (you will need it in a moment).
Why Gemini instead of Claude for classification?
You already have a Claude Code subscription for development. For the AI classification runtime, Gemini 2.5 Flash offers 1,500 free requests per day with no credit card required and no expiration. This keeps the project at $0 cost while still giving you production-quality AI classification.
Now install the Python packages your project needs. Switch back to your terminal window.
- Make sure your terminal is still in the email-assistant folder.
- Install the required packages by running this command:
pip install google-api-python-client==2.194.0 google-auth-oauthlib==1.3.1 google-genai==1.73.1
What do these packages do?
- google-api-python-client is Google's official library for calling REST APIs like Gmail and Calendar.
- google-auth-oauthlib handles the OAuth 2.0 sign-in flow that opens your browser for Google authentication.
- google-genai is the current Gemini SDK for calling Gemini models from Python.
- Verify the installation succeeded by running:
pip show google-genai
You should see package details including Version: 1.73.1 in the output.
Seeing 'pip is not recognized'?
Try using pip3 instead of pip. If that does not work, Python may not be on your system PATH. Reinstall Python and make sure to check the "Add Python to PATH" option during installation.
help me fix pip not being recognized in my terminal
Finally, set your Gemini API key as an environment variable so your script can access it at runtime.
macOS
- Set the environment variable by running this command (replace the placeholder with your actual API key):
export GEMINI_API_KEY="your-api-key-here"
Windows
- Set the environment variable by running this command (replace the placeholder with your actual API key):
$env:GEMINI_API_KEY = "your-api-key-here"
Pro tip: this variable is session-only
This environment variable only lasts for your current terminal session. If you close and reopen your terminal, you will need to run the command again before running your script.
Keep your API key private
Never paste your API key directly into your Python code or commit it to a Git repository. Using an environment variable keeps it out of your source files.
Your environment is fully set up. You have a Google Cloud project with both APIs enabled, OAuth credentials ready to go, and all the Python packages installed. Next up, you will write the Python script that connects to Gmail and fetches your real emails.
Connect to Gmail and Fetch Recent Emails
Your Google Cloud project is ready, your APIs are enabled, and your credentials.json file is waiting in your project folder.
Now it's time to write the Python code that actually talks to Google. Your script needs to prove who you are (authentication), then ask the Gmail API for your recent emails.
In this step, get ready to:
- Build an OAuth 2.0 authentication flow that lets your script access Google on your behalf.
- Fetch recent email metadata (subjects, senders, dates) from your Gmail inbox.
- Run the script and verify it pulls real emails from your account.
Build the OAuth authentication flow
Before your script can read any emails, it needs to prove to Google that you gave it permission. This is what OAuth 2.0 handles.
The authentication function opens your browser, lets you sign in to Google, and then saves a token.json file so you don't have to sign in again every time you run the script.
- Open Claude Code in your project folder (the one containing credentials.json).
- Ask Claude Code to create a Python file called email_assistant.py with the following pieces:
Claude Code should generate code that looks like this:
import os
import datetime
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
# OAuth scopes: read-only Gmail + full Calendar access
SCOPES = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/calendar",
]
def authenticate_google():
"""Authenticate with Google and return credentials."""
creds = None
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
"credentials.json", SCOPES
)
creds = flow.run_local_server(port=0)
with open("token.json", "w") as token:
token.write(creds.to_json())
return creds
What does this code do?
- SCOPES tells Google exactly what your script is allowed to do. gmail.readonly means it can read emails but never send, delete, or modify them.
- The function first checks for a saved token.json file. If it exists and is still valid, it skips the sign-in entirely.
- If no valid token exists, InstalledAppFlow opens your browser for Google sign-in. After you approve, it saves the token so the next run is automatic.
What are API scopes?
API scopes are permissions you request from Google. They follow the principle of least privilege. Your script only asks for what it needs (reading emails and managing calendar events), nothing more.
You won't see a change yet. The authentication function needs a main() function to call it, which you will add after building the email fetching function next.
Fetch recent email metadata
Your script can now authenticate, but it doesn't do anything with that access yet. The next function pulls your recent emails from the Gmail API.
The function only fetches metadata (subject lines, senders, and dates). It never downloads full email bodies, which keeps your data private.
- Ask Claude Code to add a fetch_recent_emails() function below authenticate_google() with these requirements:
Claude Code should generate a function that looks like this:
def fetch_recent_emails(service, max_results=20, days_back=3):
"""Fetch recent email metadata from Gmail."""
after_date = (
datetime.datetime.now() - datetime.timedelta(days=days_back)
).strftime("%Y/%m/%d")
query = f"after:{after_date}"
results = (
service.users()
.messages()
.list(userId="me", q=query, maxResults=max_results)
.execute()
)
messages = results.get("messages", [])
emails = []
for msg in messages:
msg_data = (
service.users()
.messages()
.get(
userId="me",
id=msg["id"],
format="metadata",
metadataHeaders=["Subject", "From", "Date"],
)
.execute()
)
headers = msg_data.get("payload", {}).get("headers", [])
email_info = {
"id": msg["id"],
"snippet": msg_data.get("snippet", ""),
"subject": "",
"sender": "",
"date": "",
}
for header in headers:
name = header.get("name", "")
if name == "Subject":
email_info["subject"] = header.get("value", "(No Subject)")
elif name == "From":
email_info["sender"] = header.get("value", "Unknown")
elif name == "Date":
email_info["date"] = header.get("value", "")
emails.append(email_info)
return emails
What does this code do?
- The function calculates a date 3 days ago and builds a Gmail search query like after:2026/04/22 to filter out older emails.
- messages().list() returns a list of message IDs matching the query. Each ID is then used to fetch that message's metadata individually.
- format='metadata' tells the Gmail API to return only the headers you specify. No email bodies are ever downloaded.
- The snippet field is a short preview (roughly 100 characters) that Gmail includes automatically with the metadata response.
Why metadata only?
Downloading full email bodies would expose sensitive content. By using format='metadata', you limit the data your script can see to subject lines, senders, dates, and short previews. This is enough for AI classification while respecting your privacy.
You won't see a change yet. The next substep adds a main() function that ties authentication and fetching together.
Run the script and verify email fetching
Your two core functions are ready. Now you need a main() function that calls them in sequence and prints the results so you can verify everything works.
- Ask Claude Code to add a main() function at the bottom of email_assistant.py that authenticates with Google, builds a Gmail service, fetches recent emails, and prints each email's subject, sender, date, and snippet.
Your main() function should look like this:
def main():
"""Main function to run the email assistant."""
print("Authenticating with Google...")
creds = authenticate_google()
gmail_service = build("gmail", "v1", credentials=creds)
print("Fetching recent emails...")
emails = fetch_recent_emails(gmail_service)
print(f"Found {len(emails)} emails.")
if not emails:
print("No recent emails found.")
return
# Print each email's details
for email in emails:
print(f"\nSubject: {email['subject']}")
print(f"From: {email['sender']}")
print(f"Date: {email['date']}")
print(f"Preview: {email['snippet'][:100]}")
print("\nDone! Your AI Email Assistant has finished.")
if __name__ == "__main__":
main()
What does this code do?
- authenticate_google() gets your Google credentials, opening a browser window if needed.
- build("gmail", "v1", credentials=creds) creates a Gmail API service object that your fetch function uses to make requests.
- The print loop displays each email's subject, sender, date, and a 100-character preview of the snippet.
- if __name__ == "__main__" ensures the script only runs when you execute it directly, not when it's imported as a module.
- Save email_assistant.py.
- Run the script in PowerShell by running:
python email_assistant.py
Seeing a Google warning page? Don't freak out
The first time you run the script, your browser opens to a Google sign-in page. After signing in, you will see a warning that says "Google hasn't verified this app" with a message about the app being in testing mode.
This is expected. Your app is in testing mode because you set it up as an External app in Step 1. Click Continue to proceed. You are the developer and the test user, so it is safe to continue.
On the next screen, click Continue again to grant the app read access to Gmail and Calendar access. Your terminal should then show your recent emails.
- Check your terminal output. You should see a list of your recent emails with their subjects, senders, dates, and short previews.
- Open your Gmail inbox in a browser and compare. The subjects and senders in the terminal output should match what you see in Gmail.
What should I see?
Your terminal should print something like:
Authenticating with Google...
Fetching recent emails...
Found 15 emails.
Subject: Meeting reminder: Team standup
From: Jane Smith <jane@example.com>
Date: Fri, 25 Apr 2026 10:30:00 -0400
Preview: Hi team, just a reminder that we have our weekly standup at 11 AM...
The exact emails will be different because these are your real inbox messages. The number of emails depends on how many you received in the last 3 days.
Script not working?
- If you see FileNotFoundError: credentials.json, make sure the credentials.json file from Step 1 is in the same folder as email_assistant.py.
- If the browser opens but you get "Access blocked," go back to the Google Cloud Console and confirm you added your Gmail address as a test user on the OAuth consent screen.
- If you see Found 0 emails, check that your Gmail account has received emails in the last 3 days. You can also try sending yourself a test email and running the script again.
- If you see a ModuleNotFoundError, go back to Step 1 and run the pip install command again to install the required packages.
Still stuck? Help me debug my email_assistant.py script.
Notice that a new token.json file appeared in your project folder. This stores your Google credentials so the script won't ask you to sign in again on future runs.
Awesome, I've got everything!
Great. Double check you have saved email_assistant.py.
I'd like to double check the full code
Here is the full email_assistant.py file as it should look at the end of this step.
import os
import datetime
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
# OAuth scopes: read-only Gmail + full Calendar access
SCOPES = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/calendar",
]
def authenticate_google():
"""Authenticate with Google and return credentials."""
creds = None
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
"credentials.json", SCOPES
)
creds = flow.run_local_server(port=0)
with open("token.json", "w") as token:
token.write(creds.to_json())
return creds
def fetch_recent_emails(service, max_results=20, days_back=3):
"""Fetch recent email metadata from Gmail."""
after_date = (
datetime.datetime.now() - datetime.timedelta(days=days_back)
).strftime("%Y/%m/%d")
query = f"after:{after_date}"
results = (
service.users()
.messages()
.list(userId="me", q=query, maxResults=max_results)
.execute()
)
messages = results.get("messages", [])
emails = []
for msg in messages:
msg_data = (
service.users()
.messages()
.get(
userId="me",
id=msg["id"],
format="metadata",
metadataHeaders=["Subject", "From", "Date"],
)
.execute()
)
headers = msg_data.get("payload", {}).get("headers", [])
email_info = {
"id": msg["id"],
"snippet": msg_data.get("snippet", ""),
"subject": "",
"sender": "",
"date": "",
}
for header in headers:
name = header.get("name", "")
if name == "Subject":
email_info["subject"] = header.get("value", "(No Subject)")
elif name == "From":
email_info["sender"] = header.get("value", "Unknown")
elif name == "Date":
email_info["date"] = header.get("value", "")
emails.append(email_info)
return emails
def main():
"""Main function to run the email assistant."""
print("Authenticating with Google...")
creds = authenticate_google()
gmail_service = build("gmail", "v1", credentials=creds)
print("Fetching recent emails...")
emails = fetch_recent_emails(gmail_service)
print(f"Found {len(emails)} emails.")
if not emails:
print("No recent emails found.")
return
# Print each email's details
for email in emails:
print(f"\nSubject: {email['subject']}")
print(f"From: {email['sender']}")
print(f"Date: {email['date']}")
print(f"Preview: {email['snippet'][:100]}")
print("\nDone! Your AI Email Assistant has finished.")
if __name__ == "__main__":
main()
Your script can now authenticate with Google and pull real emails from your inbox. Next up, you will send those emails to Gemini AI to classify and prioritize them automatically.
Classify Emails with Gemini AI
In the previous step, you connected to Gmail and fetched your recent emails. Right now your script prints raw data: subject lines, senders, and dates.
In this step, you'll send that email data to Gemini 2.5 Flash for AI-powered classification. Gemini will sort each email into priority categories and your script will print a clean daily summary.
In this step, get ready to:
- Build a classification function that sends email data to Gemini 2.5 Flash.
- Create a summary display that organizes emails by priority.
- Run the classifier on your real inbox to see AI-powered email sorting.
Build the classification function
Your script already fetches email metadata. The next piece is a function that formats those emails into a prompt, sends the prompt to Gemini, and parses the AI's JSON response back into structured data your code can use.
- In email_assistant.py, add the following function below the fetch_recent_emails function:
def classify_emails(emails):
"""Use Gemini 2.5 Flash to classify emails into categories."""
# Create a Gemini client using the API key from environment variables
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
# Format each email into a numbered text block for the prompt
email_descriptions = []
for i, email in enumerate(emails):
email_descriptions.append(
f"{i + 1}. Subject: {email['subject']}\n"
f" From: {email['sender']}\n"
f" Preview: {email['snippet']}"
)
email_text = "\n\n".join(email_descriptions)
# Build the classification prompt with category definitions
prompt = f"""You are an email assistant. Classify each email below into exactly one category:
- "urgent": Requires action, has a deadline, or is time-sensitive
- "opportunity": Scholarships, internships, job postings, events, career fairs, programs
- "informational": Updates, newsletters, announcements that are good to know
- "low_priority": Promotions, marketing, spam-like content
For each email, also provide a short one-sentence reason for the classification.
Return your response as a JSON array with objects containing:
- "email_number": the number of the email (integer)
- "category": one of the four categories above
- "reason": a short explanation
Here are the emails:
{email_text}
Return ONLY the JSON array, no other text."""
# Send the prompt to Gemini 2.5 Flash
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
)
# Strip markdown code fences if Gemini wraps the response
response_text = response.text.strip()
if response_text.startswith("```"):
lines = response_text.split("\n")
response_text = "\n".join(lines[1:-1])
# Parse the JSON array of classifications
classifications = json.loads(response_text)
# Merge classification results back into email objects
for classification in classifications:
idx = classification["email_number"] - 1
if 0 <= idx < len(emails):
emails[idx]["category"] = classification["category"]
emails[idx]["reason"] = classification["reason"]
return emails
What does this code do?
- genai.Client(api_key=...) creates a Gemini API client using the key you saved as an environment variable in Step 1.
- The loop formats each email into a numbered text block so Gemini can reference them by number in its response.
- The prompt defines four categories (urgent, opportunity, informational, low_priority) and asks Gemini to return structured JSON with a category and reason for each email.
- The code fence stripping handles a quirk where Gemini sometimes wraps JSON in markdown backticks.
- Finally, json.loads() parses the response and merges each classification back into the original email objects.
- Save email_assistant.py.
You won't see a change yet. The next function will display the classified results.
Seeing a red underline on genai or json?
- Make sure from google import genai and import json are both at the top of your file. These were added in Step 2.
- Verify you installed the google-genai package in Step 1. Run pip install google-genai==1.73.1 if you're not sure.
- help me fix import errors in my email assistant script
Create the summary display
The classification function returns emails with category and reason fields attached. Now you need a function that groups these results by priority and prints a formatted daily summary.
- In email_assistant.py, add the following function below classify_emails:
def display_summary(classified_emails):
"""Display a formatted summary of classified emails."""
# Define category display settings in priority order
categories = {
"urgent": {"title": "URGENT - Requires Action", "icon": "\ud83d\udd34", "emails": []},
"opportunity": {"title": "OPPORTUNITIES", "icon": "\ud83d\udfe1", "emails": []},
"informational": {"title": "INFORMATIONAL", "icon": "\ud83d\udd35", "emails": []},
"low_priority": {"title": "LOW PRIORITY", "icon": "\u26aa", "emails": []},
}
# Sort each email into its matching category bucket
for email in classified_emails:
category = email.get("category", "low_priority")
if category in categories:
categories[category]["emails"].append(email)
# Print the summary header
print("\n" + "=" * 60)
print(" AI EMAIL ASSISTANT - DAILY SUMMARY")
print("=" * 60)
# Print each category section with its emails
for cat_key in ["urgent", "opportunity", "informational", "low_priority"]:
cat = categories[cat_key]
if cat["emails"]:
print(f"\n{cat['icon']} {cat['title']} ({len(cat['emails'])})")
print("-" * 40)
for email in cat["emails"]:
print(f" Subject: {email['subject']}")
print(f" From: {email['sender']}")
print(f" Reason: {email.get('reason', 'N/A')}")
print()
return categories
What does this code do?
- The categories dictionary defines display settings for each priority level, including a title, color icon, and an empty list to collect matching emails.
- The first loop sorts each email into the right bucket using the category field that classify_emails attached earlier.
- The second loop prints each section in priority order: urgent first, low priority last. Only categories with emails get printed.
- The function returns the categories dictionary so later code can use the sorted results.
- Save email_assistant.py.
You won't see a change yet. The next substep wires these functions into your main flow so you can run the full classifier.
Run the classifier on your real inbox
Both functions are ready. Now you need to call them from your main() function so the script fetches emails, classifies them, and prints the summary.
- In email_assistant.py, scroll down to the main() function.
- Find this line:
if not emails:
print("No recent emails found.")
return
- Add the following code directly below that return statement, at the end of the main() function:
# Classify emails using Gemini AI
print("Classifying emails with AI...")
classified_emails = classify_emails(emails)
# Display the prioritized summary
categories = display_summary(classified_emails)
What does this code do?
classify_emails(emails) sends your email list to Gemini and returns the same list with category and reason fields added to each email. display_summary(classified_emails) groups them by priority and prints the formatted daily summary.
- Save email_assistant.py.
- Make sure your Gemini API key is still set in your PowerShell session by running:
$env:GEMINI_API_KEY = "your-api-key-here"
Why do I have to set this again?
Environment variables set with $env: in PowerShell only last for the current session. If you closed and reopened PowerShell since Step 1, the variable is gone. Run the command above with your actual API key to set it again.
- Run your updated script by running this command:
python email_assistant.py
After the script authenticates and fetches your emails, you should see a formatted daily summary printed in your terminal. It starts with a header that says AI EMAIL ASSISTANT - DAILY SUMMARY.
Each email is grouped under its category (URGENT, OPPORTUNITIES, INFORMATIONAL, or LOW PRIORITY) with the subject, sender, and the AI's reason for the classification.
Seeing an API key error or empty response?
- If you see API key not valid, double-check that you copied the full key from Google AI Studio and that there are no extra spaces.
- If you see a json.JSONDecodeError, Gemini may have returned unexpected text. Try running the script again. Gemini occasionally returns slightly different formatting between runs.
- If you see GEMINI_API_KEY is None, your environment variable is not set. Run the $env:GEMINI_API_KEY command from above again.
- help me fix my Gemini API call in my email assistant
Do the categories look right?
Review your summary and compare the AI's classifications against your own judgment. Gemini is making decisions based only on the subject line and a short snippet. Most classifications will be reasonable, but edge cases are normal. The point is that AI handles the bulk sorting so you can focus on what matters.
Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
I'd like to double check the full code
Here is the full email_assistant.py file up to this point. Compare your code against this:
import os
import json
import datetime
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from google import genai
# OAuth scopes: read-only Gmail + full Calendar access
SCOPES = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/calendar",
]
def authenticate_google():
"""Authenticate with Google and return credentials."""
creds = None
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
"credentials.json", SCOPES
)
creds = flow.run_local_server(port=0)
with open("token.json", "w") as token:
token.write(creds.to_json())
return creds
def fetch_recent_emails(service, max_results=20, days_back=3):
"""Fetch recent email metadata from Gmail."""
after_date = (
datetime.datetime.now() - datetime.timedelta(days=days_back)
).strftime("%Y/%m/%d")
query = f"after:{after_date}"
results = (
service.users()
.messages()
.list(userId="me", q=query, maxResults=max_results)
.execute()
)
messages = results.get("messages", [])
emails = []
for msg in messages:
msg_data = (
service.users()
.messages()
.get(
userId="me",
id=msg["id"],
format="metadata",
metadataHeaders=["Subject", "From", "Date"],
)
.execute()
)
headers = msg_data.get("payload", {}).get("headers", [])
email_info = {
"id": msg["id"],
"snippet": msg_data.get("snippet", ""),
"subject": "",
"sender": "",
"date": "",
}
for header in headers:
name = header.get("name", "")
if name == "Subject":
email_info["subject"] = header.get("value", "(No Subject)")
elif name == "From":
email_info["sender"] = header.get("value", "Unknown")
elif name == "Date":
email_info["date"] = header.get("value", "")
emails.append(email_info)
return emails
def classify_emails(emails):
"""Use Gemini 2.5 Flash to classify emails into categories."""
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
email_descriptions = []
for i, email in enumerate(emails):
email_descriptions.append(
f"{i + 1}. Subject: {email['subject']}\n"
f" From: {email['sender']}\n"
f" Preview: {email['snippet']}"
)
email_text = "\n\n".join(email_descriptions)
prompt = f"""You are an email assistant. Classify each email below into exactly one category:
- "urgent": Requires action, has a deadline, or is time-sensitive
- "opportunity": Scholarships, internships, job postings, events, career fairs, programs
- "informational": Updates, newsletters, announcements that are good to know
- "low_priority": Promotions, marketing, spam-like content
For each email, also provide a short one-sentence reason for the classification.
Return your response as a JSON array with objects containing:
- "email_number": the number of the email (integer)
- "category": one of the four categories above
- "reason": a short explanation
Here are the emails:
{email_text}
Return ONLY the JSON array, no other text."""
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
)
response_text = response.text.strip()
if response_text.startswith("```"):
lines = response_text.split("\n")
response_text = "\n".join(lines[1:-1])
classifications = json.loads(response_text)
for classification in classifications:
idx = classification["email_number"] - 1
if 0 <= idx < len(emails):
emails[idx]["category"] = classification["category"]
emails[idx]["reason"] = classification["reason"]
return emails
def display_summary(classified_emails):
"""Display a formatted summary of classified emails."""
categories = {
"urgent": {"title": "URGENT - Requires Action", "icon": "🔴", "emails": []},
"opportunity": {"title": "OPPORTUNITIES", "icon": "🟡", "emails": []},
"informational": {"title": "INFORMATIONAL", "icon": "🔵", "emails": []},
"low_priority": {"title": "LOW PRIORITY", "icon": "⚪", "emails": []},
}
for email in classified_emails:
category = email.get("category", "low_priority")
if category in categories:
categories[category]["emails"].append(email)
print("\n" + "=" * 60)
print(" AI EMAIL ASSISTANT - DAILY SUMMARY")
print("=" * 60)
for cat_key in ["urgent", "opportunity", "informational", "low_priority"]:
cat = categories[cat_key]
if cat["emails"]:
print(f"\n{cat['icon']} {cat['title']} ({len(cat['emails'])})")
print("-" * 40)
for email in cat["emails"]:
print(f" Subject: {email['subject']}")
print(f" From: {email['sender']}")
print(f" Reason: {email.get('reason', 'N/A')}")
print()
return categories
def main():
"""Main function to run the email assistant."""
print("Authenticating with Google...")
creds = authenticate_google()
gmail_service = build("gmail", "v1", credentials=creds)
calendar_service = build("calendar", "v3", credentials=creds)
print("Fetching recent emails...")
emails = fetch_recent_emails(gmail_service)
print(f"Found {len(emails)} emails.")
if not emails:
print("No recent emails found.")
return
print("Classifying emails with AI...")
classified_emails = classify_emails(emails)
categories = display_summary(classified_emails)
print("\nDone! Your AI Email Assistant has finished.")
if __name__ == "__main__":
main()
Your email assistant now classifies your inbox with AI and shows you a prioritized daily summary. Next up, you'll add Google Calendar integration so you can push urgent items and opportunities straight to your calendar.
Add Google Calendar Integration
Your email assistant can now read your inbox and classify every message with AI. The summary is useful on its own, but the real power comes from turning those insights into action.
In this step, you will connect to the Google Calendar API and build a scheduling flow. Your assistant will let you pick urgent and opportunity emails from the summary and push them straight to your calendar as events.
In this step, get ready to:
- Build a function that creates Google Calendar events from classified emails.
- Add an interactive prompt so you can pick which emails to schedule.
- Run the full pipeline end-to-end and verify events on your calendar.
Build the calendar event creation function
The Calendar API's events().insert() method lets you create events on a user's calendar programmatically. Each event needs a title, description, start time, end time, and timezone.
- In Claude Code, prompt it to add a create_calendar_event function to email_assistant.py. Describe that the function should take a calendar service, an email dictionary, a date, and an optional time, then insert an event into the user's primary calendar.
- Verify that Claude Code's output matches this code:
def create_calendar_event(calendar_service, email, event_date, event_time="09:00"):
"""Create a Google Calendar event from an email."""
# Build start and end timestamps (event lasts 1 hour)
start_datetime = f"{event_date}T{event_time}:00"
end_hour = int(event_time.split(":")[0]) + 1
end_datetime = f"{event_date}T{end_hour:02d}:{event_time.split(':')[1]}:00"
# Structure the calendar event with email details
event = {
"summary": f"[Email] {email['subject']}",
"description": (
f"From: {email['sender']}\n"
f"Category: {email.get('category', 'N/A')}\n"
f"Reason: {email.get('reason', 'N/A')}\n"
f"Preview: {email['snippet']}"
),
"start": {
"dateTime": start_datetime,
"timeZone": "America/New_York",
},
"end": {
"dateTime": end_datetime,
"timeZone": "America/New_York",
},
"reminders": {
"useDefault": False,
"overrides": [
{"method": "popup", "minutes": 30},
],
},
}
# Insert the event into the user's primary Google Calendar
created_event = (
calendar_service.events()
.insert(calendarId="primary", body=event)
.execute()
)
return created_event
What does this code do?
- The function builds a start time from the date and time you pass in, then calculates an end time one hour later.
- summary uses the email subject prefixed with [Email] so you can spot assistant-created events on your calendar.
- description packs the sender, category, AI reason, and snippet into the event body for context.
- calendarId='primary' targets your main Google Calendar. A 30-minute popup reminder is included by default.
Pro tip: change the timezone
The code defaults to America/New_York. If you are in a different timezone, update both timeZone values in the start and end dictionaries to match yours (e.g. America/Chicago, America/Los_Angeles, Europe/London).
- Save email_assistant.py.
You will not see a change yet. The next function builds the interactive prompt that calls this one.
Add an interactive prompt for scheduling
A calendar function is only useful if the user can choose which emails to schedule. This next function displays the urgent and opportunity emails, lets you pick items by number, and prompts for a date and time before creating each event.
- In Claude Code, prompt it to add a schedule_from_summary function below create_calendar_event. Describe that it should take the calendar service and the categories dictionary from display_summary, list urgent and opportunity emails, let the user pick by number, prompt for a date and time with sensible defaults, then create each event.
- Verify that Claude Code's output matches this code:
def schedule_from_summary(calendar_service, categories):
"""Prompt user to schedule calendar events from priority emails."""
# Combine urgent and opportunity emails into one schedulable list
schedulable = categories["urgent"]["emails"] + categories["opportunity"]["emails"]
if not schedulable:
print("\nNo urgent or opportunity emails to schedule.")
return
print("\n" + "=" * 60)
print(" SCHEDULE TO CALENDAR")
print("=" * 60)
print("\nEmails available to schedule:")
# Display each schedulable email with its category icon
for i, email in enumerate(schedulable):
icon = "\U0001f534" if email.get("category") == "urgent" else "\U0001f7e1"
print(f" {i + 1}. {icon} {email['subject']}")
print(f"\nEnter email numbers to schedule (comma-separated), or 'skip':")
choice = input("> ").strip()
if choice.lower() == "skip":
print("Skipped scheduling.")
return
# Default to tomorrow if the user does not enter a date
tomorrow = (datetime.datetime.now() + datetime.timedelta(days=1)).strftime(
"%Y-%m-%d"
)
selected_indices = [int(x.strip()) - 1 for x in choice.split(",") if x.strip().isdigit()]
# For each selected email, prompt for date and time then create the event
for idx in selected_indices:
if 0 <= idx < len(schedulable):
email = schedulable[idx]
print(f"\nScheduling: {email['subject']}")
date_input = input(f" Date (YYYY-MM-DD) [{tomorrow}]: ").strip()
time_input = input(f" Time (HH:MM) [09:00]: ").strip()
event_date = date_input if date_input else tomorrow
event_time = time_input if time_input else "09:00"
created = create_calendar_event(
calendar_service, email, event_date, event_time
)
print(f" Created! View at: {created.get('htmlLink')}")
What does this code do?
- The function combines all urgent and opportunity emails into a single list. Low priority and informational items are excluded since they rarely need calendar slots.
- The user types comma-separated numbers (like 1,3) to select emails, or skip to exit without scheduling anything.
- For each selection, the function prompts for a date and time with sensible defaults (tomorrow at 9 AM). Pressing Enter accepts the defaults.
- After creating each event, it prints the Google Calendar link so you can verify it immediately.
- Save email_assistant.py.
You will not see a change yet. The next substep wires these functions into your main pipeline.
Run the full pipeline end-to-end
Your two new functions are ready, but the main() function does not call them yet. You need to add a calendar service connection and the scheduling call.
- In email_assistant.py, scroll down to your main() function.
- Find this line:
gmail_service = build("gmail", "v1", credentials=creds)
- Add this line directly below it to create the calendar service connection:
calendar_service = build("calendar", "v3", credentials=creds)
- Next, find this line near the bottom of main():
categories = display_summary(classified_emails)
- Add these two lines directly below it:
schedule_from_summary(calendar_service, categories)
print("\nDone! Your AI Email Assistant has finished.")
What do these additions do?
- build("calendar", "v3", credentials=creds) creates an authenticated connection to the Google Calendar API, using the same OAuth credentials you already have from Gmail.
- schedule_from_summary() runs after the summary is displayed, giving you the option to push emails to your calendar.
Your updated main() function should now look like this:
def main():
"""Main function to run the email assistant."""
print("Authenticating with Google...")
creds = authenticate_google()
gmail_service = build("gmail", "v1", credentials=creds)
calendar_service = build("calendar", "v3", credentials=creds)
print("Fetching recent emails...")
emails = fetch_recent_emails(gmail_service)
print(f"Found {len(emails)} emails.")
if not emails:
print("No recent emails found.")
return
print("Classifying emails with AI...")
classified_emails = classify_emails(emails)
categories = display_summary(classified_emails)
schedule_from_summary(calendar_service, categories)
print("\nDone! Your AI Email Assistant has finished.")
if __name__ == "__main__":
main()
Awesome, I've got everything!
Double check you have saved email_assistant.py before running it.
I'd like to double check the full code
Here is the complete email_assistant.py file with all functions from every step. Compare your file against this:
import os
import json
import datetime
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from google import genai
# OAuth scopes: read-only Gmail + full Calendar access
SCOPES = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/calendar",
]
def authenticate_google():
"""Authenticate with Google and return credentials."""
creds = None
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
"credentials.json", SCOPES
)
creds = flow.run_local_server(port=0)
with open("token.json", "w") as token:
token.write(creds.to_json())
return creds
def fetch_recent_emails(service, max_results=20, days_back=3):
"""Fetch recent email metadata from Gmail."""
after_date = (
datetime.datetime.now() - datetime.timedelta(days=days_back)
).strftime("%Y/%m/%d")
query = f"after:{after_date}"
results = (
service.users()
.messages()
.list(userId="me", q=query, maxResults=max_results)
.execute()
)
messages = results.get("messages", [])
emails = []
for msg in messages:
msg_data = (
service.users()
.messages()
.get(
userId="me",
id=msg["id"],
format="metadata",
metadataHeaders=["Subject", "From", "Date"],
)
.execute()
)
headers = msg_data.get("payload", {}).get("headers", [])
email_info = {
"id": msg["id"],
"snippet": msg_data.get("snippet", ""),
"subject": "",
"sender": "",
"date": "",
}
for header in headers:
name = header.get("name", "")
if name == "Subject":
email_info["subject"] = header.get("value", "(No Subject)")
elif name == "From":
email_info["sender"] = header.get("value", "Unknown")
elif name == "Date":
email_info["date"] = header.get("value", "")
emails.append(email_info)
return emails
def classify_emails(emails):
"""Use Gemini 2.5 Flash to classify emails into categories."""
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
email_descriptions = []
for i, email in enumerate(emails):
email_descriptions.append(
f"{i + 1}. Subject: {email['subject']}\n"
f" From: {email['sender']}\n"
f" Preview: {email['snippet']}"
)
email_text = "\n\n".join(email_descriptions)
prompt = f"""You are an email assistant. Classify each email below into exactly one category:
- "urgent": Requires action, has a deadline, or is time-sensitive
- "opportunity": Scholarships, internships, job postings, events, career fairs, programs
- "informational": Updates, newsletters, announcements that are good to know
- "low_priority": Promotions, marketing, spam-like content
For each email, also provide a short one-sentence reason for the classification.
Return your response as a JSON array with objects containing:
- "email_number": the number of the email (integer)
- "category": one of the four categories above
- "reason": a short explanation
Here are the emails:
{email_text}
Return ONLY the JSON array, no other text."""
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
)
response_text = response.text.strip()
if response_text.startswith("```"):
lines = response_text.split("\n")
response_text = "\n".join(lines[1:-1])
classifications = json.loads(response_text)
for classification in classifications:
idx = classification["email_number"] - 1
if 0 <= idx < len(emails):
emails[idx]["category"] = classification["category"]
emails[idx]["reason"] = classification["reason"]
return emails
def display_summary(classified_emails):
"""Display a formatted summary of classified emails."""
categories = {
"urgent": {"title": "URGENT - Requires Action", "icon": "\U0001f534", "emails": []},
"opportunity": {"title": "OPPORTUNITIES", "icon": "\U0001f7e1", "emails": []},
"informational": {"title": "INFORMATIONAL", "icon": "\U0001f535", "emails": []},
"low_priority": {"title": "LOW PRIORITY", "icon": "\u26aa", "emails": []},
}
for email in classified_emails:
category = email.get("category", "low_priority")
if category in categories:
categories[category]["emails"].append(email)
print("\n" + "=" * 60)
print(" AI EMAIL ASSISTANT - DAILY SUMMARY")
print("=" * 60)
for cat_key in ["urgent", "opportunity", "informational", "low_priority"]:
cat = categories[cat_key]
if cat["emails"]:
print(f"\n{cat['icon']} {cat['title']} ({len(cat['emails'])})")
print("-" * 40)
for email in cat["emails"]:
print(f" Subject: {email['subject']}")
print(f" From: {email['sender']}")
print(f" Reason: {email.get('reason', 'N/A')}")
print()
return categories
def create_calendar_event(calendar_service, email, event_date, event_time="09:00"):
"""Create a Google Calendar event from an email."""
start_datetime = f"{event_date}T{event_time}:00"
end_hour = int(event_time.split(":")[0]) + 1
end_datetime = f"{event_date}T{end_hour:02d}:{event_time.split(':')[1]}:00"
event = {
"summary": f"[Email] {email['subject']}",
"description": (
f"From: {email['sender']}\n"
f"Category: {email.get('category', 'N/A')}\n"
f"Reason: {email.get('reason', 'N/A')}\n"
f"Preview: {email['snippet']}"
),
"start": {
"dateTime": start_datetime,
"timeZone": "America/New_York",
},
"end": {
"dateTime": end_datetime,
"timeZone": "America/New_York",
},
"reminders": {
"useDefault": False,
"overrides": [
{"method": "popup", "minutes": 30},
],
},
}
created_event = (
calendar_service.events()
.insert(calendarId="primary", body=event)
.execute()
)
return created_event
def schedule_from_summary(calendar_service, categories):
"""Prompt user to schedule calendar events from priority emails."""
schedulable = categories["urgent"]["emails"] + categories["opportunity"]["emails"]
if not schedulable:
print("\nNo urgent or opportunity emails to schedule.")
return
print("\n" + "=" * 60)
print(" SCHEDULE TO CALENDAR")
print("=" * 60)
print("\nEmails available to schedule:")
for i, email in enumerate(schedulable):
icon = "\U0001f534" if email.get("category") == "urgent" else "\U0001f7e1"
print(f" {i + 1}. {icon} {email['subject']}")
print(f"\nEnter email numbers to schedule (comma-separated), or 'skip':")
choice = input("> ").strip()
if choice.lower() == "skip":
print("Skipped scheduling.")
return
tomorrow = (datetime.datetime.now() + datetime.timedelta(days=1)).strftime(
"%Y-%m-%d"
)
selected_indices = [int(x.strip()) - 1 for x in choice.split(",") if x.strip().isdigit()]
for idx in selected_indices:
if 0 <= idx < len(schedulable):
email = schedulable[idx]
print(f"\nScheduling: {email['subject']}")
date_input = input(f" Date (YYYY-MM-DD) [{tomorrow}]: ").strip()
time_input = input(f" Time (HH:MM) [09:00]: ").strip()
event_date = date_input if date_input else tomorrow
event_time = time_input if time_input else "09:00"
created = create_calendar_event(
calendar_service, email, event_date, event_time
)
print(f" Created! View at: {created.get('htmlLink')}")
def main():
"""Main function to run the email assistant."""
print("Authenticating with Google...")
creds = authenticate_google()
gmail_service = build("gmail", "v1", credentials=creds)
calendar_service = build("calendar", "v3", credentials=creds)
print("Fetching recent emails...")
emails = fetch_recent_emails(gmail_service)
print(f"Found {len(emails)} emails.")
if not emails:
print("No recent emails found.")
return
print("Classifying emails with AI...")
classified_emails = classify_emails(emails)
categories = display_summary(classified_emails)
schedule_from_summary(calendar_service, categories)
print("\nDone! Your AI Email Assistant has finished.")
if __name__ == "__main__":
main()
- Save email_assistant.py.
- Make sure your Gemini API key environment variable is still set. If you opened a new PowerShell window since Step 1, set it again by running:
$env:GEMINI_API_KEY = "your-api-key-here"
- Run the full pipeline by running this command:
python email_assistant.py
The script will fetch your emails, classify them with AI, and display the daily summary. After the summary, you will see the SCHEDULE TO CALENDAR section listing your urgent and opportunity emails.
- Type the number of an email you want to schedule (e.g. 1) and press Enter.
- Press Enter again at the date prompt to accept tomorrow's date.
- Press Enter at the time prompt to accept the default of 09:00.
You should see a Created! message followed by a Google Calendar link. Click the link to open the event in your browser.
- Open Google Calendar in your browser at calendar.google.com.
- Navigate to tomorrow's date. You should see an event titled with [Email] followed by the email subject you scheduled.
- Click the event to verify it contains the sender, category, reason, and preview in the description.
Event not showing up on your calendar?
- If you see an HttpError 403, your OAuth token from earlier runs may not include the Calendar scope. Delete token.json from your project folder and run the script again to re-authenticate with both Gmail and Calendar permissions.
- If the event lands on the wrong day, check that you entered the date in YYYY-MM-DD format (e.g. 2026-04-26).
- If the event time looks wrong, make sure the timeZone in create_calendar_event matches your actual timezone.
- Still stuck? Help me debug my Google Calendar event creation.
Secret mission
Add VIP Sender Priority Boosting
Your email assistant treats every sender equally. But in real life, an email from your professor or a recruiter deserves more attention than a newsletter. Add VIP sender recognition to automatically boost priority for the people who matter most.
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs locally and uses free-tier APIs, so there are no ongoing costs.
Resources you used:
- Google Cloud project ("Email Assistant") with Gmail API and Calendar API enabled.
- OAuth 2.0 credentials (credentials.json and token.json files in your project folder).
- Google AI Studio API key for Gemini 2.5 Flash.
- Environment variable GEMINI_API_KEY set in PowerShell.
- Python packages: google-api-python-client, google-auth-oauthlib, google-genai.
- Local project files (email_assistant.py and optionally vip_senders.json).
Keep everything running
No action needed. Choose this if you are still actively building or plan to run the assistant daily.
- Your Google Cloud project, APIs, and OAuth credentials stay active.
- Your token.json file keeps you authenticated so you can rerun the script without signing in again.
- All APIs used in this project are free at this scale, so nothing will cost you money.
Pause - I'll come back to this later
Remove sensitive files from your local machine while keeping the cloud project intact for later.
- Delete token.json from your project folder. This revokes your current session. You will re-authenticate when you run the script again.
- Keep credentials.json in your project folder so you can re-authenticate later.
- Your Google Cloud project and enabled APIs will stay available at no cost.
- The GEMINI_API_KEY environment variable only persists for the current PowerShell session, so it will be cleared automatically when you close the terminal.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Delete your local project folder containing email_assistant.py, credentials.json, token.json, and any other files you created.
- Go to the Google Cloud Console (console.cloud.google.com).
- Open IAM & Admin, then Manage Resources. Select your "Email Assistant" project and click Delete.
- Go to Google AI Studio (aistudio.google.com). Open your API keys list and delete the key you created for this project.
- Uninstall the Python packages if you no longer need them:
pip uninstall google-api-python-client google-auth-oauthlib google-genai -y
- Optionally, revoke the app's access to your Google account. Go to myaccount.google.com/permissions and remove the "Email Assistant" app.
Mission accomplished
That's a Wrap!
Nice work! You just built a real AI-powered email assistant that reads your Gmail inbox, classifies your emails with Gemini AI, and pushes important items straight to your Google Calendar.
Here's what you learned:
- Connected to Google's Gmail and Calendar APIs using OAuth 2.0 authentication, fetching real email data from your inbox while respecting privacy with metadata-only access.
- Used Gemini 2.5 Flash to classify and prioritize your emails into actionable categories, then generated a clean daily summary ranked by urgency.
- Built a full end-to-end pipeline that creates Google Calendar events directly from priority emails, with interactive scheduling and custom date/time selection.
- Added VIP sender priority boosting so emails from important contacts automatically get ranked higher in your daily summary.
Ready to quiz yourself?