AI Email Digest with Gmail and Ollama

Build a Python tool that reads Gmail and summarizes emails with local AI.

Introduction

30 Second Summary

Your inbox fills up faster than you can read it. Every morning starts with the same problem: dozens of unread emails, and no quick way to tell which ones actually matter.

In this project, you will build a Python command-line tool that connects to your Gmail account and uses a local AI model to summarize each email into one sentence. Your email data never leaves your computer.

What You'll Build

Run a single command in your terminal and watch a formatted digest appear, listing each recent email's sender, subject, and a concise AI-generated summary.

By the end of this project, you'll have:

  • A Python script that connects to your inbox through the Gmail API, authenticating in read-only mode to fetch your 5 most recent emails with their sender, subject, and date.
  • AI-powered summarization of each email into a one-line summary, running entirely on your machine through Ollama and the Gemma 3 model.
  • A reusable terminal command that produces an inbox briefing whenever you need a quick look at your email.
  • Secret Mission: Add priority detection that flags emails needing a response.

Do I need anything installed before starting?

This project assumes you have Python 3 and a code editor (like VS Code) on your Windows machine. The guide walks you through setting up everything else, including Ollama and your Google Cloud credentials.

Before We Start

Before you write any code, take a moment to lock in what you're about to build and why it matters.

Set Up Ollama and Pull the AI Model

You've committed to what you're building. Now it's time to get the AI engine running on your machine.

Before connecting to Gmail or writing any Python code, you'll install a tool called Ollama and download an AI model. Setting up the AI first means you can test it independently before adding any complexity.

In this step, get ready to:
  • Install Ollama on your Windows machine.
  • Download the Gemma 3 AI model.
  • Test the model by chatting with it in your terminal.
Download and install Ollama

Ollama is a free, open-source tool that runs AI models directly on your computer. Instead of sending your email content to a cloud service, everything stays on your machine.

  • Open https://ollama.com/download in your browser.
  • Click Download for Windows to download OllamaSetup.exe.
  • Run OllamaSetup.exe to install Ollama.
  • If Windows asks whether to allow the installation, confirm and proceed.

What is Ollama?

Ollama downloads and runs large language models locally on your machine. Your data never leaves your computer, which is perfect for processing private content like emails.

Ollama requires Windows 10 or later.

Once the installer finishes, you need to open a brand new terminal window. Existing terminals won't recognize the ollama command until you start a fresh session.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
Pull the Gemma 3 model

AI models need to be downloaded before you can use them. Gemma 3 is a model by Google with 4 billion parameters. It's small enough to run on most machines while being powerful enough for email summarization.

  • Download the Gemma 3 model by running this command:
ollama pull gemma3

✔️ I see a download progress bar

Ollama is installed and working. The model is downloading now.

Wait for the download to finish before moving on. This downloads approximately 3.3GB, so it may take a few minutes depending on your internet connection.

ⓧ 'ollama' is not recognized

This means your terminal can't find the Ollama command. Try these fixes:

  • Make sure you opened a completely new terminal window after installing Ollama. Don't reuse a terminal that was already open before the install.
  • Confirm the installer actually finished. Re-run OllamaSetup.exe if you're not sure.
  • Try restarting your computer, then open a new terminal and run the command again.

Grab a coffee while it downloads

The Gemma 3 model is approximately 3.3GB. On a typical home connection this can take 5 to 10 minutes. You'll see a progress bar in your terminal while it downloads.

Test the model

The download is done. Before you go any further, you should confirm the model actually works by chatting with it directly in your terminal.

  • Start an interactive chat session with Gemma 3 by running this command:
ollama run gemma3

You'll see a prompt waiting for your input. Before you type anything, what do you think the model will do when you say "Hello"?

  • Type Hello and press Enter.

The model responds with a greeting or helpful message. This is a local LLM running entirely on your machine. Every response is generated by the Gemma 3 model you just downloaded.

  • Type /bye and press Enter to exit the chat session.

Model not responding?

  • If the model takes a long time on the first response, that's normal. The first message loads the model into memory, which can take 30 seconds or more depending on your hardware.
  • If you see an error about insufficient memory, close other applications to free up RAM. The Gemma 3 4B model needs at least a few gigabytes of available memory.

Help me troubleshoot why my Ollama model isn't responding

💡 Try a longer prompt

You can start another chat session with ollama run gemma3 and try a summarization prompt like "Summarize this in one sentence: The quarterly report shows revenue increased by 15%." This previews what the model will do for your emails later.

Your AI engine is up and running. Next up, you'll connect to Gmail and pull in your actual email data for the model to summarize.

Connect to Gmail and See Raw Email Data

In Step 1, you proved that the Gemma 3 AI can generate text on your machine. Now you need real emails to feed it.

In this step, you will connect to your actual Gmail inbox through the Gmail API. You will create a Google Cloud project, set up secure authentication, and write a Python script that pulls your five most recent emails.

In this step, get ready to:
  • Set up a Google Cloud project with Gmail API access.
  • Install the Gmail API Python libraries.
  • Build and run a script that fetches your 5 most recent emails.
Create your project folder

You need a dedicated folder on your Desktop to hold all your project files.

  • Open your terminal. Press the Windows key to open your search bar, type PowerShell, and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir email-digest
cd email-digest
  • Confirm the folder was created by running:
pwd

You should see a path ending in email-digest.

Enable the Gmail API in Google Cloud Console

The Gmail API lets your Python script read emails from your inbox. To use it, you first need to create a Google Cloud project and switch the API on.

  • Open your browser and go to console.cloud.google.com.
  • Sign in with the Google account that owns the Gmail inbox you want to read.
  • At the top of the page, click the project dropdown (it may say Select a project).
  • Click New Project.
  • Enter Email Digest as the project name.
  • Click Create.
  • Wait a few seconds, then select your new Email Digest project from the project dropdown at the top of the page.

What is Google Cloud Console?

Google Cloud Console is a web dashboard where you manage Google's cloud services. You are creating a project here so Google knows which app is requesting access to your Gmail data.

  • In the left sidebar, click the hamburger menu (three horizontal lines), then navigate to APIs & Services.
  • Click Library.
  • In the search bar, type Gmail API and press Enter.
  • Click Gmail API in the results.
  • Click Enable.

You should see the Gmail API overview page with a blue Manage button, confirming the API is now active.

Set up OAuth credentials

Your script needs permission to read your emails. OAuth 2.0 is the industry standard for granting apps limited access to your account without sharing your password.

First, you will configure the OAuth consent screen. This is the page Google shows you when your script asks for permission.

  • In Google Cloud Console, open the hamburger menu and navigate to Google Auth platform.
  • Click Branding. If you see a message that says the Google Auth platform is not configured yet, click Get Started.
  • Under App Information, enter Email Digest in the App name field.
  • In User support email, select your email address from the dropdown.
  • Click Next.
  • Under Audience, select External.
  • Click Next.

Why select External?

Personal Gmail accounts are not part of a Google Workspace organization. Selecting External lets your app work with any Google account. Since the app stays in testing mode, only users you explicitly add can use it.

  • Under Contact Information, enter your email address.
  • Click Next.
  • Under Finish, review the Google API Services User Data Policy. If you agree, select the checkbox I agree to the Google API Services: User Data Policy.
  • Click Continue.
  • Click Create.

Now you need to add the read-only email scope and register yourself as a test user.

  • Click Data Access in the left sidebar under Google Auth platform.
  • Click Add or Remove Scopes.
  • In the filter field, type gmail.readonly to narrow the list.
  • Check the box next to https://www.googleapis.com/auth/gmail.readonly.
  • Click Update.
  • Click Save.

What is the gmail.readonly scope?

A scope defines exactly what your app can do. The gmail.readonly scope grants read-only access to your inbox. Your script can view emails but can never modify, send, or delete them.

  • Click Audience in the left sidebar under Google Auth platform.
  • Under Test users, click Add users.
  • Enter your Gmail address and click Save.

Now create the OAuth client ID that your script will use to identify itself.

  • Click Clients in the left sidebar under Google Auth platform.
  • Click Create Client.
  • For Application type, select Desktop app.
  • In the Name field, enter Email Digest.
  • Click Create.

Google will show your new client ID. You now need to download the credentials file.

  • Click the download icon next to your new OAuth 2.0 Client ID to download the JSON file.
  • Rename the downloaded file to credentials.json.
  • Move credentials.json into your email-digest folder on your Desktop.
  • Back in your terminal, confirm the file arrived by running:
ls

You should see credentials.json listed in the output.

Don't see credentials.json?

  • Make sure you renamed the downloaded file to exactly credentials.json (no extra characters or spaces).
  • Confirm you moved it into the email-digest folder on your Desktop, not a subfolder or your Downloads folder.
  • Run pwd to confirm your terminal is inside the email-digest folder.

Help me find my credentials.json file.

Install the Python libraries

Your script needs Google's official Python libraries to handle authentication and talk to the Gmail API.

  • Install all three libraries by running this command:
pip install google-api-python-client==2.199.0 google-auth-httplib2==0.4.1 google-auth-oauthlib==1.4.0

You should see output ending with a success message confirming the packages were installed.

What are these libraries?

  • google-api-python-client is the core library for calling any Google API from Python.
  • google-auth-httplib2 handles HTTP transport for authentication requests.
  • google-auth-oauthlib manages the OAuth 2.0 browser sign-in flow.

🙋‍♀️ Seeing a permission error?

If pip reports a permission issue, try running the command with the --user flag to install the packages for your user account only.

Help me fix my pip install error.

Write the authentication code

Your project needs one Python file that handles everything: authenticating with Gmail, fetching emails, and printing the results. You will build it in chunks, starting with the authentication logic.

  • Open VS Code. Press the Windows key, type VS Code, and press Enter.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the email-digest folder on your Desktop, then click Select Folder.
  • Create a new file by clicking File then New File.
  • Save it immediately as email_digest.py by pressing Ctrl+S and entering the filename.
  • Add the imports, constants, and authentication function by pasting this code into email_digest.py:
import os
import base64
import re
from datetime import datetime

# Google authentication and API libraries
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

# Read-only scope ensures the script can never modify your emails
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
MAX_EMAILS = 5
MAX_BODY_LENGTH = 500


def authenticate_gmail():
    creds = None
    # Check for a saved token from a previous run
    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 build("gmail", "v1", credentials=creds)

What does this code do?

  • SCOPES locks the script to read-only Gmail access. It can never send, edit, or delete your emails.
  • authenticate_gmail() checks for a saved token.json from a previous run. If none exists, it opens your browser so you can sign in and approve access.
  • After you approve, the function saves the token to token.json so you do not need to sign in again on future runs.
  • The function returns a Gmail API service object that your other functions will use to fetch emails.
  • Save email_digest.py by pressing Ctrl+S.
  • Verify the imports work by running:
python email_digest.py

The script should exit silently with no errors. If you see an import error, double-check that the pip install command from the previous substep succeeded.

Fetch and display your emails

Now add the functions that call the Gmail API and a main() function that ties everything together.

  • Below the authenticate_gmail function in email_digest.py, add the email fetching functions:
def get_header(headers, name):
    # Search through email headers to find a specific one by name
    for header in headers:
        if header["name"].lower() == name.lower():
            return header["value"]
    return ""


def fetch_emails(service):
    # Request the 5 most recent inbox messages
    results = service.users().messages().list(
        userId="me", maxResults=MAX_EMAILS, labelIds=["INBOX"]
    ).execute()
    messages = results.get("messages", [])

    emails = []
    for msg in messages:
        # Get the full message details including headers
        detail = service.users().messages().get(
            userId="me", id=msg["id"], format="full"
        ).execute()
        headers = detail["payload"].get("headers", [])

        emails.append({
            "subject": get_header(headers, "Subject"),
            "from": get_header(headers, "From"),
            "date": get_header(headers, "Date"),
        })

    return emails

What does this code do?

  • get_header() is a helper that searches an email's headers by name (like "Subject" or "From") and returns the value.
  • fetch_emails() calls the Gmail API twice for each email. First, messages().list() gets a list of message IDs. Then messages().get() fetches the full details for each one.
  • The function returns a list of dictionaries, each containing the subject, sender, and date of one email.
  • Below the fetch_emails function, add the main function that runs everything:
def main():
    print("Connecting to Gmail...")
    service = authenticate_gmail()

    print("Fetching emails...")
    emails = fetch_emails(service)

    if not emails:
        print("No emails found.")
        return

    # Loop through each email and print its headers
    for i, email in enumerate(emails, 1):
        print(f"--- Email {i} ---")
        print(f"  From:    {email['from']}")
        print(f"  Subject: {email['subject']}")
        print(f"  Date:    {email['date']}")
        print()


if __name__ == "__main__":
    main()

What does this code do?

  • main() connects to Gmail, fetches the 5 most recent emails, and prints the subject, sender, and date for each one.
  • The if __name__ == "__main__" block ensures main() runs when you execute the script directly from the terminal.
  • Save email_digest.py by pressing Ctrl+S.

✔️ Awesome, I've got everything!

Great. Make sure you have saved email_digest.py before moving on.

ⓧ I'd like to double check the full code

Your email_digest.py file should contain three blocks of code, in this order:

  • The imports, constants (SCOPES, MAX_EMAILS, MAX_BODY_LENGTH), and the authenticate_gmail() function.
  • The get_header() and fetch_emails() functions.
  • The main() function and the if __name__ == "__main__" block at the bottom.

Scroll up and confirm each chunk is present and in order. Make sure there are no duplicate imports or functions.

Before you run the script, think about what you expect to happen. This is the first time your script contacts Gmail. Do you think it will immediately print emails, or will something else happen first?

  • Run your script:
python email_digest.py

What will happen on the first run?

Your browser will open automatically, asking you to sign in with your Google account and grant read-only access to your inbox.

Because your app is in testing mode and has not been verified by Google, you may see a warning screen. Click Advanced, then click Go to Email Digest (unsafe) to continue. This is expected for development apps.

  • In the browser, sign in with the Gmail account you added as a test user.
  • If you see a warning about the app being unverified, click Advanced, then click the link that says Go to Email Digest (unsafe).
  • Review the permissions the app is requesting (read-only Gmail access) and click Continue.

After you approve, switch back to your terminal. You should see the subject, sender, and date for your 5 most recent emails printed out.

Not seeing any email output?

  • Make sure credentials.json is in the same folder as email_digest.py. Run ls to confirm both files appear.
  • Check that you added your Gmail address as a test user in the Audience section of Google Auth platform.
  • If the browser did not open, make sure you are running the script from inside the email-digest folder. Run pwd to check.
  • If you see a token error, delete token.json from your project folder and run the script again to re-authenticate.

Help me troubleshoot my Gmail API authentication.

You now have a working connection to your real Gmail inbox. Your script fetches the subject, sender, and date for each email. But where is the email body? The actual message content is locked inside encoded data that you have not unpacked yet. In the next step, you will crack open that encoding and see what raw email content really looks like.

Parse the Email Body

In Step 2, you connected to Gmail and printed the headers for your five most recent emails. You can see who sent each message and when it arrived.

But the actual email content is still missing. The Gmail API returns email bodies as base64url-encoded strings, often wrapped in HTML tags and split across multiple MIME parts. Before the AI can summarize anything useful, you need to decode and clean this raw data into readable text.

In this step, get ready to:
  • Decode email bodies from the Gmail API's base64-encoded payload.
  • Strip HTML tags and clean the body text for readability.
Add HTML stripping and body extraction

Emails from the Gmail API arrive in two messy forms. The text itself is base64url-encoded, so it looks like a jumble of random characters. Many emails also contain HTML formatting instead of plain text, filled with <div>, <style>, and <script> tags that bury the actual words.

You need two helper functions to handle this. The first strips HTML tags from a string. The second navigates the Gmail API's MIME structure and decodes the raw body data.

  • In email_digest.py, find the get_header function. Copy the following code below it to add the strip_html function:
def strip_html(text):
    # Remove style and script blocks entirely
    text = re.sub(r"<style[^>]*>.*?</style>", "", text, flags=re.DOTALL)
    text = re.sub(r"<script[^>]*>.*?</script>", "", text, flags=re.DOTALL)
    # Replace remaining HTML tags with spaces
    text = re.sub(r"<[^>]+>", " ", text)
    # Collapse extra whitespace into single spaces
    text = re.sub(r"\s+", " ", text)
    return text.strip()

What does this code do?

This function takes a string of HTML and strips it down to plain text. It first removes <style> and <script> blocks entirely, since those contain CSS and JavaScript that are never useful text.

Then it replaces all remaining HTML tags with spaces and collapses repeated whitespace into single spaces. The result is clean, readable text.

  • Directly below strip_html, add the get_email_body function by copying this code:
def get_email_body(payload):
    body = ""

    # Handle multipart emails (most common format)
    if "parts" in payload:
        for part in payload["parts"]:
            mime_type = part.get("mimeType", "")
            # Prefer plain text over HTML
            if mime_type == "text/plain" and part.get("body", {}).get("data"):
                body = base64.urlsafe_b64decode(part["body"]["data"]).decode("utf-8", errors="replace")
                break
            elif mime_type == "text/html" and part.get("body", {}).get("data"):
                raw_html = base64.urlsafe_b64decode(part["body"]["data"]).decode("utf-8", errors="replace")
                body = strip_html(raw_html)
        # Check for nested multipart structures
        if not body:
            for part in payload["parts"]:
                if "parts" in part:
                    body = get_email_body(part)
                    if body:
                        break
    # Handle single-part emails
    elif payload.get("body", {}).get("data"):
        raw = base64.urlsafe_b64decode(payload["body"]["data"]).decode("utf-8", errors="replace")
        if payload.get("mimeType") == "text/html":
            body = strip_html(raw)
        else:
            body = raw

    # Truncate to keep output manageable
    return body[:MAX_BODY_LENGTH] if body else "(no body content)"

What does this code do?

This function navigates the Gmail API's MIME structure to find the actual email text. Emails can arrive in two shapes: multipart (body split across text/plain and text/html sections) or single-part (body stored directly in the payload).

The function prefers text/plain because it is already clean text. If only text/html is available, it decodes the HTML and runs it through strip_html. It also handles nested multipart structures by calling itself recursively.

Every body is decoded from base64url using base64.urlsafe_b64decode and truncated to 500 characters via the MAX_BODY_LENGTH constant you defined in Step 2.

  • Save email_digest.py.
  • Confirm the new functions don't introduce any syntax errors by running:
python email_digest.py

You should see the same header-only output as before (Subject, From, Date). The new functions are defined but nothing calls them yet.

Seeing a syntax error?

  • Make sure strip_html is placed after get_header and before get_email_body. The order matters because get_email_body calls strip_html.
  • Check that each function definition starts at the leftmost indentation level, not nested inside another function.
  • Verify your regex strings all use raw string notation (r"..."). Missing the r prefix can cause escape character issues.

Help me debug a syntax error in my email_digest.py after adding strip_html and get_email_body functions.

Display the cleaned email body

Your two helper functions are ready. Now you need to wire them into the existing code so the script actually uses them when fetching and displaying emails.

  • In email_digest.py, scroll down to the fetch_emails function.
  • Find this line inside the for msg in messages: loop:
        headers = detail["payload"].get("headers", [])

        emails.append({
            "subject": get_header(headers, "Subject"),
            "from": get_header(headers, "From"),
            "date": get_header(headers, "Date"),
        })
  • Replace it with this updated version that extracts the body and includes it in the email dictionary:
        headers = detail["payload"].get("headers", [])
        body = get_email_body(detail["payload"])

        emails.append({
            "subject": get_header(headers, "Subject"),
            "from": get_header(headers, "From"),
            "date": get_header(headers, "Date"),
            "body": body,
        })
  • Now scroll down to the main function and find this line:
        print(f"  Date:    {email['date']}")
        print()
  • Add a new print line between them so the block looks like this:
        print(f"  Date:    {email['date']}")
        print(f"  Body:    {email['body']}")
        print()
  • Save email_digest.py.

Before you run the script, think about what you expect to see differently compared to last time. Will the output look different now?

  • Run the updated script:
python email_digest.py

Each email now shows a Body: line with the decoded, cleaned text from the email. You should see readable sentences instead of encoded gibberish, stripped of any HTML tags and limited to the first 500 characters.

Why are some bodies short or empty?

Some emails are image-heavy with almost no text content. Others use deeply nested MIME structures or store their body as an attachment rather than inline data. If you see (no body content) for an email, it means the message didn't contain extractable text in a standard MIME part.

🙋‍♀️ Not seeing any body text?

  • Check that you added body = get_email_body(detail["payload"]) on the line directly after the headers = ... line in fetch_emails.
  • Make sure "body": body, is inside the emails.append({...}) dictionary. A missing comma after the "date" line will cause an error.
  • Verify the print line uses email['body'] (with square brackets and quotes), not email.body.

Help me debug why my email bodies aren't showing up after updating fetch_emails and main.

✔️ Awesome, I've got everything!

Great. Double check you've saved email_digest.py and move on to the next step.

ⓧ I'd like to double check the full code

Your email_digest.py is getting longer, so scroll through it and confirm the following sections are present, in order:

  • Imports: os, base64, re, datetime, and the four Google auth/API imports.
  • Constants: SCOPES, MAX_EMAILS, and MAX_BODY_LENGTH.
  • authenticate_gmail() function (from Step 2).
  • get_header() function (from Step 2).
  • strip_html() function (new in this step).
  • get_email_body() function (new in this step).
  • fetch_emails() function with body = get_email_body(detail["payload"]) and "body": body in the dictionary.
  • main() function with print(f" Body: {email['body']}") after the Date print line.

Your emails now have readable body text. But those bodies are still walls of text that take time to scan through. In the next step, you'll connect the local Gemma 3 model to summarize each email into a single concise sentence.

Summarize Emails with AI

Your script now fetches emails and shows their cleaned body text. But reading through walls of text for each email is not much better than opening Gmail itself.

This is where Ollama earns its place. You will connect the ollama Python library to send each email to your local Gemma 3 model and get back a concise one-line summary. The result is a formatted digest you can scan in seconds.

In this step, get ready to:
  • Install the ollama Python library.
  • Create a function that sends each email to Gemma 3 for summarization.
  • Update the digest output to show AI-generated summaries instead of raw body text.
Install the Ollama Python library

The ollama package gives your Python script a direct line to the Gemma 3 model running on your machine. Instead of sending HTTP requests manually, you call a single function and get the AI's response back as a Python object.

  • Install the library by running this command in your terminal:
pip install ollama==0.6.2
  • Wait for the installation to finish. You should see Successfully installed ollama-0.6.2 in the output.

Installation failed?

Make sure you are running the command in the same terminal and virtual environment where you installed the Google API libraries in Step 2.

help me troubleshoot my pip install for the ollama Python library

Add the import and summarize function
  • In email_digest.py, find this line near the top of your imports:
from googleapiclient.discovery import build
  • Add this import line directly below it:
from ollama import chat
  • Now scroll down past your fetch_emails function, and add this new function above main():
def summarize_email(subject, body):
    # Send the email to Gemma 3 and ask for a one-sentence summary
    response = chat(
        model="gemma3",
        messages=[
            {
                "role": "system",
                "content": (
                    "You are an email summarizer. "
                    "Given an email subject and body, write a single concise sentence "
                    "summarizing what the email is about. Only output the summary sentence, "
                    "nothing else."
                ),
            },
            {
                "role": "user",
                "content": f"Subject: {subject}\n\nBody: {body}",
            },
        ],
    )
    return response.message.content.strip()

What does this function do?

The chat() function sends a conversation to your local Gemma 3 model through Ollama. The system message tells the AI exactly how to behave: summarize in one sentence, nothing more.

The user message passes the email's subject and body as the content to summarize. The response comes back as a Python object, and response.message.content holds the AI's text.

  • Save email_digest.py. The function is defined but not called yet. The next chunk wires it into your digest output.
Update main() to display the AI digest

Right now your main() function prints raw body text for each email. You will replace it with a version that calls summarize_email() and formats everything into a clean digest with a header, separators, and a footer.

  • In email_digest.py, find your existing main() function. It currently starts like this:
def main():
    print("Connecting to Gmail...")
    service = authenticate_gmail()

    print("Fetching emails...")
    emails = fetch_emails(service)
  • Delete the entire main() function (from def main(): down to the last print() call inside it), and replace it with this updated version:
def main():
    # Print the digest header with current date and time
    print("=" * 60)
    print(f"  EMAIL DIGEST - {datetime.now().strftime('%Y-%m-%d %H:%M')}")
    print("=" * 60)
    print()

    print("Connecting to Gmail...")
    service = authenticate_gmail()

    print("Fetching emails...")
    emails = fetch_emails(service)

    if not emails:
        print("No emails found.")
        return

    print(f"Summarizing {len(emails)} emails with AI...\n")

    for i, email in enumerate(emails, 1):
        # Send each email to Gemma 3 for a one-line summary
        summary = summarize_email(email["subject"], email["body"])

        print(f"--- Email {i} of {len(emails)} ---")
        print(f"  From:    {email['from']}")
        print(f"  Subject: {email['subject']}")
        print(f"  Date:    {email['date']}")
        print(f"  Summary: {summary}")
        print()

What changed in main()?

  • A header line prints EMAIL DIGEST with the current date and time so you know when the digest was generated.
  • Each email now calls summarize_email() instead of printing the raw body. The AI summary replaces the wall of text with a single sentence.
  • The output now shows Email 1 of 5 separators so you can tell emails apart at a glance.

You also need a footer that closes the digest. Still inside main(), add these lines after the for loop (at the same indentation level as the loop itself):

    # Print the digest footer with total count
    print("=" * 60)
    print(f"  Digest complete. {len(emails)} emails summarized.")
    print("=" * 60)

What does this footer do?

These three lines close off the digest with a matching border and a count of how many emails were processed. It gives the output a polished, scannable shape from header to footer.

  • Save email_digest.py.

Before you run this, what do you think the output will look like compared to the last time you ran the script? Will the emails still show the raw body text?

  • Run the script by entering this command in your terminal:
python email_digest.py

The script connects to Gmail, fetches your 5 most recent emails, and sends each one to Gemma 3. Each email takes a few seconds to summarize, so be patient.

You should see a formatted digest with a header, and for each email a From, Subject, Date, and a one-sentence AI-generated Summary. The raw body text is gone, replaced by a clean one-liner from the AI.

Seeing a connection error?

If you see an error like Failed to connect to Ollama, your Ollama service is not running. Open a separate terminal window and run ollama serve to start it. Then try running the script again.

If you see an error about the model not being found, make sure you pulled gemma3 in Step 1. Run ollama pull gemma3 to re-download it.

help me fix my Ollama connection or model error when running email_digest.py

✔️ Awesome, I've got everything!

Great. Double check you have saved email_digest.py and your digest is printing AI summaries for each email.

ⓧ I'd like to double check the full code

Your email_digest.py file should contain all of these sections in order. Scroll through your file and confirm each one is present:

  • Imports: os, base64, re, datetime, the Google auth libraries, and from ollama import chat.
  • Constants: SCOPES, MAX_EMAILS, and MAX_BODY_LENGTH.
  • Functions (in order): authenticate_gmail(), get_header(), strip_html(), get_email_body(), fetch_emails(), summarize_email(), main().
  • The main() function should print a header with EMAIL DIGEST, call summarize_email() for each email, and end with a footer showing the total count.
  • Entry point: if __name__ == "__main__": main() at the very end of the file.

Your email digest is alive. One command, and you get an AI-powered briefing of your inbox, entirely on your own machine. Next up, the Secret Mission challenges you to make this tool even smarter by adding priority detection that flags which emails actually need your attention.

Secret mission

Add Priority Detection

Your digest summarizes what each email says, but it doesn't tell you which ones need your attention. In this mission, you'll upgrade the AI to detect whether each email requires action and flag high-priority messages with a visual indicator in your output.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses only the free tier of the Gmail API, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Google Cloud project with Gmail API enabled and OAuth credentials.
  • Local project files: email_digest.py, credentials.json, and token.json.
  • Gemma 3 model downloaded to Ollama's local storage (~3.3GB on disk).
  • Ollama installed on your machine.
  • Python packages: google-api-python-client, google-auth-httplib2, google-auth-oauthlib, and ollama.
  • App access granted to your Google account for the Email Digest app.

Keep everything running

No action needed. Choose this if you plan to keep using or extending this email digest tool.

  • Ollama and the Gemma 3 model stay installed for future AI projects.
  • The Gmail API is free within generous quotas (1,200,000 quota units per day), so your Google Cloud project costs nothing to keep.
  • Your token.json file keeps you authenticated so you can re-run the script anytime.

Pause - I'll come back to this later

Free up disk space by removing the AI model, but keep everything else so you can resume easily.

  • Remove the Gemma 3 model to free up ~3.3GB of disk space by running this command:
  • When you come back, re-download the model by running ollama pull gemma3.
ollama rm gemma3
  • Keep your project files (email_digest.py, credentials.json, token.json) so you can pick up right where you left off.
  • Keep Ollama installed so you only need to re-pull the model to resume.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below.

  • Remove the Gemma 3 model by running this command:
ollama rm gemma3
  • Uninstall Ollama from your machine.
  • Press the Windows key to open your search bar.
  • Type Add or remove programs and press Enter.
  • Find Ollama in the list, click it, and select Uninstall.
  • Follow the uninstaller prompts. When asked whether to keep your models, select the option to remove them.
  • Delete your project files by running this command in the folder containing your project:
del email_digest.py credentials.json token.json
  • Uninstall the Python packages by running:
pip uninstall google-api-python-client google-auth-httplib2 google-auth-oauthlib ollama -y
  • Revoke the app's access to your Google account.
  • Open your browser and go to https://myaccount.google.com/permissions.
  • Find Email Digest (or whatever you named the app in the OAuth consent screen) in the list.
  • Click on it and select Remove Access.
  • Delete the Google Cloud project.
  • Go to the Manage resources page in the Google Cloud Console.
  • Select your Email Digest project from the list and click Delete.
  • Enter the project ID in the confirmation dialog and click Shut down.

Nice Work!

Nice Work!

Nice work! You just built a fully functional AI-powered email digest that runs entirely on your machine.

You've learned how to:

  • Authenticate with Gmail using OAuth 2.0 and fetch real emails through the Gmail API, including parsing raw MIME-encoded bodies into clean, readable text.
  • Run a local Gemma 3 AI model through Ollama and connect it to a Python script using the ollama library to summarize email content into concise one-liners.
  • Produce a formatted terminal digest that turns a messy inbox into a scannable briefing with one command.
  • Secret Mission: Added priority detection that flags emails needing action vs. informational emails, complete with [ACTION NEEDED] and [FYI] tags and a summary footer.

Ready to quiz yourself?