Read and Send Emails with Gmail API

Build a Python CLI tool that reads and sends Gmail messages via OAuth 2.0.

Introduction

30 Second Summary

Every app that asks you to "Sign in with Google" is using the same authentication system you are about to learn. Understanding this flow unlocks the ability to automate almost any Google service programmatically.

In this project, you will build a local Python tool that connects to your Gmail account via the official Gmail API, reads your recent emails, and sends new messages from the command line.

What You'll Build

A Python command-line toolkit that authenticates with Google's OAuth 2.0, lists and reads Gmail messages, and sends emails programmatically.

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

  • An OAuth 2.0 authentication flow configured in Google Cloud Console that grants your script access to Gmail.
  • A reusable authentication module that handles token creation and refresh automatically.
  • Python scripts that read and send emails through the Gmail API.
  • Secret Mission: build an auto-reply bot that detects unread emails matching a keyword and sends a canned response.

Are there any prerequisites?

You need basic Python knowledge (variables, functions, imports). No prior experience with Google APIs or OAuth is needed.

Set Up Your Python Environment

Before you can talk to the Gmail API, you need a working Python environment on your computer.

In this step, you will install Python (if needed), create a dedicated project folder, and install the Google client libraries that handle authentication and API calls.

In this step, get ready to:
  • Verify Python 3.10+ and pip are installed.
  • Create a project folder and virtual environment.
  • Install the Google client libraries.
Verify Python 3.10+ and pip are installed

The Gmail API client libraries require Python 3.10 or higher. Let's confirm you have the right version installed.

  • 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.
  • Check your Python version by running this command:
python3 --version

Windows users

On Windows, the command may be python --version instead of python3 --version. Try both if one does not work.

✔️ I see version 3.10 or higher

You're all set. Your output looks something like Python 3.10.x or Python 3.11.x or higher. Move on to the next step below.

ⓧ I see an older version

If you see a version below 3.10 (for example Python 3.8.x or Python 3.9.x), you need to install a newer version.

On macOS, the system may ship with an older Python stub that is not suitable for development. You need to install a newer version separately.

  • Go to python.org/downloads and download the latest Python installer for your operating system.
  • Run the installer. On Windows, make sure to check Add Python to PATH before clicking Install Now.
  • Close and reopen your terminal, then run python3 --version again to confirm the new version.

macOS path shadowing

After installing from python.org, the new version may live at /Library/Frameworks/Python.framework/Versions/3.x/bin/python3. If python3 --version still shows the old version, close and reopen Terminal so the updated PATH takes effect.

ⓧ Command not found

Python is not installed on your machine yet.

  • Go to python.org/downloads and download the latest Python installer for your operating system.
  • Run the installer. On Windows, make sure to check Add Python to PATH before clicking Install Now.
  • Close and reopen your terminal after installation completes.
  • Run python3 --version to verify the installation. You should see Python 3.10 or higher.

Windows alternative

On Windows, you can also install Python from the Microsoft Store. Search for "Python" and install the version published by the Python Software Foundation.

  • Confirm pip is available by running:
pip3 --version

pip3 not found?

If pip3 is not recognized, try pip --version (Windows) or python3 -m pip --version. Pip is bundled with Python 3.10+ by default, so reinstalling Python from python.org usually fixes this.

Help me troubleshoot pip not being found on my system.

Create your project folder and virtual environment

A virtual environment keeps this project's packages separate from your system Python. This prevents version conflicts with other projects.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir gmail-api-project
cd gmail-api-project
  • Create a virtual environment inside the project folder by running:
python3 -m venv venv

What does this command do?

python3 -m venv venv tells Python to create a new virtual environment in a folder called venv. This folder contains its own copy of Python and pip, isolated from your system installation.

  • Activate the virtual environment:

macOS

source venv/bin/activate

Windows

venv\Scripts\Activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

Make sure you are inside the gmail-api-project folder before running the activate command. Run ls (macOS) or dir (Windows) and confirm you see a venv folder listed.

On Windows, if you get a permissions error, run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell first, then try the activate command again.

Help me activate my Python virtual environment.

Install the Google client libraries

Google provides official Python libraries that handle OAuth authentication and Gmail API calls. You need three packages working together.

  • Install the required packages by running:
pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib

What are these packages?

  • google-api-python-client is the core library for calling any Google API, including Gmail.
  • google-auth-httplib2 provides HTTP transport for authenticated requests.
  • google-auth-oauthlib handles the OAuth 2.0 login flow that lets your script access Gmail on your behalf.
  • Verify the installation succeeded by running:
pip list | grep google

You should see google-api-python-client, google-auth-httplib2, and google-auth-oauthlib listed with version numbers.

Installation failed?

Make sure (venv) is showing in your terminal prompt. If it is not, go back and activate the virtual environment first. Running pip install outside the venv may require admin permissions and could fail.

Help me fix my pip install failing for Google client libraries.

Your Python environment is ready to go. Next up, you will create a Google Cloud project and enable the Gmail API so your scripts have something to authenticate against.

Create a Google Cloud Project and Enable the Gmail API

Your Python environment is ready. Now you need to connect your script to Google's systems so it can access your Gmail inbox.

Google requires every app that accesses its APIs to be registered in a Google Cloud project. This project acts like an identity badge for your app. In this step, you will create that project, turn on the Gmail API, and set up the OAuth 2.0 credentials your Python script needs to prove it has permission to read and send emails.

In this step, get ready to:
  • Create a Google Cloud project and enable the Gmail API.
  • Configure the OAuth consent screen for testing.
  • Create and download OAuth 2.0 credentials.
Create a Google Cloud project and enable the Gmail API

The Gmail API is hosted in Google Cloud. Before your script can use it, you need a Google Cloud project to hold your API settings and credentials.

  • Open your browser and go to console.cloud.google.com.
  • Sign in with your Google account (the Gmail account you want to read from).
  • At the top of the page, click the project selector dropdown (it may say Select a project or show an existing project name).
  • Click New Project.
  • In the Project name field, enter Gmail API Project.
  • Click Create.
  • Wait a few seconds for the project to be created. A notification appears in the top right confirming your project is ready.
  • Make sure your new project is selected in the project selector dropdown at the top of the page.

Now enable the Gmail API for this project.

  • In the search bar at the top of the console, type Gmail API and press Enter.
  • Click the Gmail API result from the Marketplace.
  • Click Enable.

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

What is an API project?

A Google Cloud project groups your API credentials, billing, and usage quotas in one place. Every app that talks to a Google API needs one. Think of it as your app's registration with Google.

Don't see the Enable button?

Make sure you have Gmail API Project selected in the project dropdown at the top. If you see Disable instead of Enable, the API is already active and you can move on.

Help me enable the Gmail API in my Google Cloud project.

Configure the OAuth consent screen

Before your app can ask users to sign in, Google needs to know what your app is called and what kind of access it requests. This information is displayed on the OAuth consent screen.

  • In the Google Cloud Console, click the navigation menu (hamburger icon) in the top left.
  • Navigate to Google Auth platform, then click Branding.
  • If you see a message saying the Google Auth platform is not configured yet, click Get Started.

What is a consent screen?

When your Python script runs for the first time, it opens a browser window asking you to sign in and grant access. The consent screen is what you see during that flow. It shows the app name, the developer's email, and what permissions the app is requesting.

Fill out the consent screen configuration:

  • Under App Information, enter Gmail API Project in the App name field.
  • In User support email, select your email address from the dropdown.
  • Click Next.
  • Under Audience, select External.
  • Click Next.
  • Under Contact Information, enter your email address.
  • Click Next.
  • Under Finish, review the Google API Services User Data Policy.
  • Check the box for I agree to the Google API Services: User Data Policy.
  • Click Continue, then click Create.

Because you selected External, you need to add yourself as a test user so your script can authenticate.

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

Why External instead of Internal?

The Internal option is only available if your Google account belongs to a Google Workspace organization (like a company or school). Personal Gmail accounts must use External. When set to External with no published verification, only users you explicitly add as test users can authorize the app.

Your app will stay in "Testing" mode, which is fine for a local development project.

Don't see the Audience option?

If your Google account is part of a Workspace organization and you only see Internal, that is fine for this project. Select Internal and skip adding test users. Your organization's users (including you) can authorize without being listed as test users.

Help me figure out which audience setting to use for my OAuth consent screen.

Create OAuth 2.0 credentials and download the client file

Your consent screen is configured. Now you need to create the actual credentials file that your Python script will use to identify itself to Google during the sign-in flow.

  • In the left sidebar, navigate to Google Auth platform, then click Clients.
  • Click Create Client.
  • For Application type, select Desktop app.
  • In the Name field, enter Gmail Desktop Client.
  • Click Create.

A dialog appears showing your newly created OAuth 2.0 client. You will see a Download button for the JSON file.

  • Click the download button to save the JSON file to your computer.
  • Find the downloaded file (it will have a long auto-generated name) and rename it to credentials.json.
  • Move credentials.json into your gmail-api-project folder (the same folder where your virtual environment lives).

Confirm the file is in the right place by switching back to your terminal and running:

ls credentials.json

You should see credentials.json printed in the output, confirming the file is in your project folder.

What is credentials.json?

This file contains your app's client ID and client secret. Your Python script reads it to start the OAuth sign-in flow. It does not contain your personal password or Gmail data.

Never commit this file to a public repository. It identifies your app to Google, and anyone with it could impersonate your app in the OAuth flow.

Can't find the downloaded file?

Check your Downloads folder. The file name starts with client_secret_ followed by a long string of numbers and ends with .json. Rename it to credentials.json and move it to your gmail-api-project folder.

If you closed the dialog, go back to Google Auth platform then Clients. Click the download icon next to your Gmail Desktop Client entry.

Help me find and move my credentials.json file.

Your Google Cloud project is configured and your credentials file is ready. Next up, you will write the Python authentication module that uses this file to sign in and manage access tokens.

Build the Authentication Module

Your Google Cloud project is configured and your credentials are ready. Now you need a way for your Python scripts to actually prove their identity to Google.

In this step, you will build a reusable authentication module that handles the entire OAuth 2.0 flow. This module checks for saved tokens, refreshes expired ones, and runs the browser-based consent flow when needed.

In this step, get ready to:
  • Create a reusable authentication module that manages OAuth tokens.
  • Test the authentication flow by signing in with your Google account.
Create the authentication module

The auth module is the foundation for every script in this project. It centralizes all token logic so your read and send scripts never worry about credentials.

  • Create a new file called auth.py in your gmail-api-project folder by opening your code editor and adding the file.

You will build auth.py in three chunks: the imports, the credentials function, and the service builder.

  • Add the following imports and scopes definition at the top of auth.py:
import os.path

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

# Define which Gmail permissions this app requests
SCOPES = [
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/gmail.send",
]

What does this code do?

The imports bring in the Google authentication libraries you installed earlier. Each handles a different part of the OAuth flow.

  • Request handles HTTP transport for refreshing tokens.
  • Credentials loads saved tokens from a file.
  • InstalledAppFlow runs the browser-based consent flow for desktop apps.
  • build creates the Gmail API service object you will use to make API calls.
  • SCOPES tells Google exactly what permissions your app needs. gmail.readonly lets you read emails. gmail.send lets you send them.

What are API scopes?

Scopes are permission boundaries. They tell the user exactly what your app can and cannot do with their account. By requesting only the scopes you need, you follow the principle of least privilege.

  • Below the SCOPES list, add the get_credentials() function:

def get_credentials():
    """Get valid user credentials from storage or run the auth flow."""
    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 function do?

This function handles three scenarios every time it runs:

  • If token.json exists with a valid token, it uses that token immediately. No browser needed.
  • If the token exists but has expired, it refreshes it silently using the refresh token.
  • If no token exists (first run), it opens your browser for the full consent flow and saves the new token to token.json.

This means after your first sign-in, future runs authenticate instantly without opening the browser.

  • Below get_credentials(), add the get_gmail_service() function and the test block:

def get_gmail_service():
    """Build and return an authorized Gmail API service instance."""
    creds = get_credentials()
    service = build("gmail", "v1", credentials=creds)
    return service


if __name__ == "__main__":
    service = get_gmail_service()
    print("Authentication successful!")
    results = service.users().labels().list(userId="me").execute()
    labels = results.get("labels", [])
    print(f"Found {len(labels)} labels in your Gmail account.")

What does this code do?

  • get_gmail_service() combines authentication and service creation into one call. Other scripts import this function directly.
  • The if __name__ == "__main__" block only runs when you execute auth.py directly. It tests authentication by listing your Gmail labels.
  • userId="me" is a special value that refers to the currently authenticated user.
  • Save auth.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved auth.py with all three sections (imports, get_credentials, and get_gmail_service).

ⓧ I'd like to double check the full code

import os.path

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

SCOPES = [
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/gmail.send",
]


def get_credentials():
    """Get valid user credentials from storage or run the auth flow."""
    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 get_gmail_service():
    """Build and return an authorized Gmail API service instance."""
    creds = get_credentials()
    service = build("gmail", "v1", credentials=creds)
    return service


if __name__ == "__main__":
    service = get_gmail_service()
    print("Authentication successful!")
    results = service.users().labels().list(userId="me").execute()
    labels = results.get("labels", [])
    print(f"Found {len(labels)} labels in your Gmail account.")
Run the authentication flow

With the module saved, you can test the full OAuth consent flow. This is the same flow apps like Slack or Notion use when they ask you to sign in with Google.

  • Make sure your virtual environment is still active in your terminal (you should see (venv) at the start of your prompt).
  • Run the authentication script:
python3 auth.py

What happens when you run this?

Since this is the first run and no token.json exists yet, the script starts a temporary local server and opens your default browser to Google's consent page. You will sign in and grant your app the requested permissions.

  • Your browser opens to a Google sign-in page. Select the Google account you used to create your Cloud project.
  • You will see a warning screen saying the app is not verified. Click Advanced, then click Go to Gmail API Project (unsafe).
  • Review the permissions the app is requesting (read email, send email) and click Continue to grant access.
  • Your browser shows a success message. Return to your terminal.

Why does it say the app is not verified?

Your app is in testing mode and has not gone through Google's verification process. This is expected for personal development projects. The warning only appears because the app requests sensitive scopes (reading and sending email). Since you own both the app and the account, it is safe to proceed.

In your terminal, you should see output similar to this:

Authentication successful!
Found 15 labels in your Gmail account.

The exact number of labels will differ based on your Gmail account. Any number greater than zero confirms a successful connection.

  • Confirm that a token.json file now exists in your gmail-api-project folder by running:
ls

You should see auth.py, credentials.json, token.json, and venv listed.

Browser did not open or seeing an error?

  • Make sure you are running this on your local machine, not a remote server or cloud shell. The OAuth flow requires a local browser.
  • Check that credentials.json is in the same folder where you ran the command. Run ls to confirm.
  • If you see a "redirect_uri_mismatch" error, confirm your OAuth client type is set to Desktop app in Google Cloud Console.

Help me troubleshoot the OAuth authentication flow

Your authentication module is complete and tested. Every future script in this project will import get_gmail_service() from this file. Next up, you will use it to read the emails in your inbox.

Read Emails from Your Inbox

Your authentication module is working and your token.json proves you can connect to Google securely.

Now it is time to use that connection for something useful. In this step, you will build a script that talks to the Gmail API to fetch your real emails and display them in the terminal.

In this step, get ready to:
  • List your 5 most recent message IDs from Gmail.
  • Retrieve email headers (sender, subject, date) for each message.
  • Decode and display the full body of your most recent email.
Create the read_emails file and list message IDs

The Gmail API uses a two-step pattern for reading emails. First, you request a list of message IDs. Then, you fetch the details for each individual message using its ID.

  • Create a new file called read_emails.py in your gmail-api-project folder.
  • Add the imports and the list_messages function by pasting the following code:
import base64

from auth import get_gmail_service


def list_messages(service, max_results=5):
    """List the most recent message IDs."""
    results = service.users().messages().list(
        userId="me", maxResults=max_results
    ).execute()
    messages = results.get("messages", [])
    return messages

What does this code do?

The import base64 line brings in Python's built-in module for decoding base64url-encoded data. You will need this later when reading email bodies.

The list_messages function calls messages().list() with userId="me" (a special value meaning "the currently authenticated user") and maxResults=5 to limit the response. The API returns only message IDs and thread IDs at this stage.

  • Save read_emails.py.
  • You won't see output yet. The next function will add header retrieval so you can see who sent each email.
Retrieve email headers for each message

A message ID alone does not tell you much. To see the sender, subject line, and date, you need to fetch each message's metadata headers individually.

  • In read_emails.py, add the get_message_headers function below list_messages:
def get_message_headers(service, msg_id):
    """Get From, Subject, and Date headers for a message."""
    message = service.users().messages().get(
        userId="me",
        id=msg_id,
        format="metadata",
        metadataHeaders=["From", "Subject", "Date"],
    ).execute()
    headers = message.get("payload", {}).get("headers", [])
    header_dict = {h["name"]: h["value"] for h in headers}
    return header_dict

What does this code do?

This function calls messages().get() with format="metadata". The metadata format returns only the message ID, labels, and email headers. It skips the body entirely, making it fast and lightweight.

The metadataHeaders parameter further narrows the response to only the three headers you care about: From, Subject, and Date. The function converts the list of header objects into a simple dictionary for easy access.

  • Save read_emails.py.
  • You won't see output yet. The next function will decode full email bodies and the main() function will tie everything together.
Add the message body decoder and run the script

Email bodies in the Gmail API are encoded in base64url format. Your script needs to decode them back into readable text. Emails can also be structured as multipart MIME messages, so the function handles both simple and multipart layouts.

  • In read_emails.py, add the get_message_body function below get_message_headers:
def get_message_body(service, msg_id):
    """Get the full message body decoded from base64url."""
    message = service.users().messages().get(
        userId="me", id=msg_id, format="full"
    ).execute()
    payload = message.get("payload", {})
    body_data = ""

    if payload.get("body", {}).get("data"):
        body_data = payload["body"]["data"]
    elif payload.get("parts"):
        for part in payload["parts"]:
            if part.get("mimeType") == "text/plain":
                body_data = part.get("body", {}).get("data", "")
                break

    if body_data:
        decoded = base64.urlsafe_b64decode(body_data).decode("utf-8")
        return decoded
    return "(No readable body found)"

What does this code do?

This function uses format="full" to get the complete message data with the body content parsed in the payload field.

Simple emails store the body directly in payload["body"]["data"]. Multipart emails (with attachments or HTML alternatives) split the body across payload["parts"]. The function checks both locations and grabs the text/plain part.

The base64.urlsafe_b64decode() call converts the encoded data back into human-readable UTF-8 text.

  • Save read_emails.py.

Now add the main() function that ties all three functions together and prints the output.

  • Add the main() function and the entry point at the bottom of read_emails.py:
def main():
    service = get_gmail_service()
    print("Fetching your 5 most recent emails...\n")

    messages = list_messages(service, max_results=5)
    if not messages:
        print("No messages found.")
        return

    for msg in messages:
        headers = get_message_headers(service, msg["id"])
        print(f"From: {headers.get('From', 'Unknown')}")
        print(f"Subject: {headers.get('Subject', '(no subject)')}")
        print(f"Date: {headers.get('Date', 'Unknown')}")
        print("-" * 50)

    print(f"\nReading full body of the most recent email...\n")
    body = get_message_body(service, messages[0]["id"])
    print(body[:500])


if __name__ == "__main__":
    main()

What does this code do?

The main() function orchestrates the full workflow. It connects to Gmail using your auth module, fetches 5 message IDs, then loops through each one to print the sender, subject, and date.

After printing all headers, it picks the first (most recent) message and decodes its full body. The body[:500] slice limits the output to the first 500 characters so your terminal stays readable.

  • Save read_emails.py.
  • Run the script by executing this command in your terminal:
python3 read_emails.py
  • You should see 5 email entries with From, Subject, and Date fields separated by dashed lines, followed by the decoded body text of your most recent email.

Not seeing any emails?

  • Make sure your Gmail account has at least one email in it. A brand new test account with an empty inbox will show "No messages found."
  • If you see an authentication error, your token.json may have expired. Delete it and run python3 auth.py again to re-authorize.
  • If you see ModuleNotFoundError: No module named 'auth', make sure your terminal is in the gmail-api-project folder where both auth.py and read_emails.py live.

Help me debug why my read_emails.py script is not returning emails from the Gmail API.

✔️ Awesome, I've got everything!

Great. Double check you have saved read_emails.py and confirmed it prints your email headers and body text.

ⓧ I'd like to double check the full code

import base64

from auth import get_gmail_service


def list_messages(service, max_results=5):
    """List the most recent message IDs."""
    results = service.users().messages().list(
        userId="me", maxResults=max_results
    ).execute()
    messages = results.get("messages", [])
    return messages


def get_message_headers(service, msg_id):
    """Get From, Subject, and Date headers for a message."""
    message = service.users().messages().get(
        userId="me",
        id=msg_id,
        format="metadata",
        metadataHeaders=["From", "Subject", "Date"],
    ).execute()
    headers = message.get("payload", {}).get("headers", [])
    header_dict = {h["name"]: h["value"] for h in headers}
    return header_dict


def get_message_body(service, msg_id):
    """Get the full message body decoded from base64url."""
    message = service.users().messages().get(
        userId="me", id=msg_id, format="full"
    ).execute()
    payload = message.get("payload", {})
    body_data = ""

    if payload.get("body", {}).get("data"):
        body_data = payload["body"]["data"]
    elif payload.get("parts"):
        for part in payload["parts"]:
            if part.get("mimeType") == "text/plain":
                body_data = part.get("body", {}).get("data", "")
                break

    if body_data:
        decoded = base64.urlsafe_b64decode(body_data).decode("utf-8")
        return decoded
    return "(No readable body found)"


def main():
    service = get_gmail_service()
    print("Fetching your 5 most recent emails...\n")

    messages = list_messages(service, max_results=5)
    if not messages:
        print("No messages found.")
        return

    for msg in messages:
        headers = get_message_headers(service, msg["id"])
        print(f"From: {headers.get('From', 'Unknown')}")
        print(f"Subject: {headers.get('Subject', '(no subject)')}")
        print(f"Date: {headers.get('Date', 'Unknown')}")
        print("-" * 50)

    print(f"\nReading full body of the most recent email...\n")
    body = get_message_body(services, messages[0]["id"])
    print(body[:500])


if __name__ == "__main__":
    main()

You can now read your inbox programmatically. Next up, you will build a script that sends emails through the Gmail API.

Send an Email

You can read your inbox programmatically. Now it is time to go the other direction and send an email entirely from your Python script.

The Gmail API requires outgoing messages to be formatted as MIME (the standard email format) and encoded as a base64url string. Python's built-in email.message module handles the formatting, and one line of encoding is all you need to bridge the gap to the API.

In this step, get ready to:
  • Create a send_email.py script that composes and sends emails through the Gmail API.
  • Send a test email to yourself and confirm delivery in your inbox.
Create the send email script

This script needs to compose a properly formatted email, encode it for the API, and call the send endpoint. You will split this into a reusable send_message function and a main function that prompts you for input.

  • Create a new file called send_email.py in your gmail-api-project folder.
  • Add the imports and the send_message function by pasting this code:
import base64
from email.message import EmailMessage

from auth import get_gmail_service


def send_message(service, to, subject, body_text):
    """Create and send an email message. Returns the sent message ID."""
    # Build a standard MIME email using Python's built-in EmailMessage
    message = EmailMessage()
    message.set_content(body_text)
    message["To"] = to
    message["Subject"] = subject

    # Encode the message as a base64url string for the Gmail API
    encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
    create_message = {"raw": encoded_message}

    # Send the message via the Gmail API
    sent_message = (
        service.users()
        .messages()
        .send(userId="me", body=create_message)
        .execute()
    )
    return sent_message

What does this code do?

EmailMessage() creates a standard MIME-formatted email. You set the recipient, subject, and body just like filling in a form.

base64.urlsafe_b64encode(message.as_bytes()).decode() converts the entire email into a base64url-encoded string. The Gmail API requires messages in this format inside the raw field.

service.users().messages().send() calls the Gmail API's send endpoint, which delivers the email to the recipients listed in the headers.

  • Below the send_message function, add the main function that prompts for user input:

def main():
    service = get_gmail_service()

    # Prompt the user for email details
    to_address = input("Enter recipient email address: ")
    subject = input("Enter subject: ")
    body = input("Enter message body: ")

    result = send_message(service, to_address, subject, body)
    print(f"\nEmail sent successfully! Message ID: {result['id']}")


if __name__ == "__main__":
    main()

What does this code do?

main() authenticates with Gmail using your existing auth.py module, then asks you to type in the recipient address, subject line, and message body.

It passes those inputs to send_message and prints the message ID that Gmail returns on success.

  • Save send_email.py.

Seeing a syntax error when you save?

Check that the def main(): line is at the same indentation level as def send_message(...). Both functions should be top-level (no leading spaces).

Also confirm you have a blank line between the end of send_message and the start of main.

help me fix a syntax error in send_email.py

✔️ Awesome, I've got everything!

Great. Double check you have saved send_email.py before moving on.

ⓧ I'd like to double check the full code

import base64
from email.message import EmailMessage

from auth import get_gmail_service


def send_message(service, to, subject, body_text):
    """Create and send an email message. Returns the sent message ID."""
    message = EmailMessage()
    message.set_content(body_text)
    message["To"] = to
    message["Subject"] = subject

    encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
    create_message = {"raw": encoded_message}

    sent_message = (
        service.users()
        .messages()
        .send(userId="me", body=create_message)
        .execute()
    )
    return sent_message


def main():
    service = get_gmail_service()

    to_address = input("Enter recipient email address: ")
    subject = input("Enter subject: ")
    body = input("Enter message body: ")

    result = send_message(service, to_address, subject, body)
    print(f"\nEmail sent successfully! Message ID: {result['id']}")


if __name__ == "__main__":
    main()
Run the script and send a test email

The safest way to test is to send an email to yourself. That way you can instantly verify delivery without bothering anyone else.

  • Run the send script by executing this command in your terminal:
python3 send_email.py
  • When prompted, enter your own Gmail address as the recipient.
  • Enter a subject like Test from Gmail API.
  • Enter a short message body like Hello from my Python script!.

What should I see?

After you press Enter on the message body, the script sends the email and prints a confirmation with the message ID:

Email sent successfully! Message ID: 18f3a2b4c5d6e7f8

The actual ID will be different. Any string of characters confirms the send was successful.

  • Open your Gmail inbox in your browser.
  • Look for the email with the subject you just entered. It should appear within a few seconds.

Email not arriving?

Check your Spam folder. Gmail sometimes routes programmatically sent emails there on the first attempt.

If you see an error about insufficient permissions, your token.json may not include the gmail.send scope. Delete token.json and run python3 auth.py to re-authorize with both scopes.

help me troubleshoot why my Gmail API send script is failing

Why base64url encoding?

The Gmail API requires the entire email message in an RFC 2822 formatted and base64url encoded string inside the raw field. This encoding makes the email safe to transmit as JSON without special characters breaking the request.

Python's base64.urlsafe_b64encode handles this conversion in a single call. The .decode() at the end converts the bytes object to a regular string that fits inside the JSON payload.

Secret mission

Build an Auto-Reply Bot

What if your inbox could handle itself? In this bonus challenge, you'll combine everything you've learned to build a bot that detects unread emails matching a keyword and fires off an automatic reply. Then it marks those emails as read so they don't get processed twice.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses the Gmail API free tier, 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 named "Gmail API Project" (includes Gmail API, OAuth client, consent screen).
  • OAuth token granting access to your Gmail account (token.json).
  • Local gmail-api-project folder (Python scripts, virtual environment, credentials.json).

Keep everything running

No action needed. Choose this if you plan to keep building on this project or experimenting with the Gmail API.

  • Your Google Cloud project stays active at no cost.
  • Your token.json remains on disk and will refresh automatically when your scripts run.
  • Your virtual environment and scripts stay ready to use.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files stay on disk so you can pick up where you left off.

  • Deactivate your virtual environment by running:
  • Your project folder and Google Cloud project remain intact for when you return.
deactivate

When you come back, reactivate the environment by navigating to your project folder and running:

cd ~/Desktop/gmail-api-project
source venv/bin/activate

Delete - I don't want to use this again

Remove all project resources and start fresh. This revokes your OAuth credentials and deletes all local files.

Revoke the app's access to your Gmail

Your token.json file grants your scripts access to your Gmail account. Revoke this access from your Google Account settings.

  • Go to myaccount.google.com and sign in.
  • Navigate to Security.
  • Find Third-party apps with account access and select it.
  • Select your "Gmail Desktop Client" app and click Remove Access.
Delete your Google Cloud project

Deleting the project removes the Gmail API configuration, OAuth client, and consent screen all at once.

  • Go to the Google Cloud Console at console.cloud.google.com.
  • Make sure your "Gmail API Project" is selected in the project dropdown at the top.
  • Navigate to IAM & Admin then Settings.
  • Click Shut down.
  • Enter the project ID to confirm, then click Shut down anyway.
Delete your local project folder

This removes all Python scripts, the virtual environment, your credentials file, and the token file.

  • Deactivate your virtual environment first (if active) by running:
deactivate
  • Delete the project folder by running:

macOS

rm -rf ~/Desktop/gmail-api-project

Windows

rmdir /s /q %USERPROFILE%\Desktop\gmail-api-project

Nice Work!

Nice Work!

You just built a complete Gmail API integration from scratch. You can now authenticate with Google, read your inbox, and send emails programmatically using Python.

You've learned how to:

  • Configure OAuth 2.0 authentication with Google Cloud Console and build a reusable token management module that handles refresh automatically.
  • List, read, and decode email messages from your inbox via the Gmail API using Python.
  • Send emails programmatically using MIME encoding and base64url encoding through the Gmail API.
  • Built an auto-reply bot that filters unread emails by keyword, sends canned responses, and marks messages as read.

Ready to quiz yourself?