Skip to main content

simplegmail

PyPI downloads

A small Python client for sending, retrieving, and modifying messages through the Gmail API.

simplegmail supports:

  • plain-text and HTML messages;
  • file attachments, Cc, Bcc, aliases, and Gmail signatures;
  • standard-library EmailMessage objects, drafts, and threaded replies;
  • Gmail search queries and common inbox filters;
  • lazy or eager attachment downloads; and
  • label changes such as read/unread, star/unstar, archive, spam, and trash.

Requirements and installation

simplegmail requires Python 3.10 or newer.

python -m pip install simplegmail

Google OAuth setup

Before using the library, create OAuth desktop credentials for a Google Cloud project:

  1. Create or select a project in the Google Cloud console.
  2. Enable the Gmail API.
  3. Configure the OAuth consent screen.
  4. Create an OAuth client ID with the application type Desktop app.
  5. Download the client JSON as client_secret.json into your application directory.

Google's Gmail Python quickstart contains the current console instructions.

The first Gmail() call opens a browser for authorization and stores the result in gmail_token.json:

from simplegmail import Gmail

gmail = Gmail()

Both paths are configurable:

gmail = Gmail(
    client_secret_file="config/client_secret.json",
    creds_file="config/gmail_token.json",
)

Treat both files as secrets and never commit them. When an external OAuth app has the publishing status Testing, Google may expire its authorization and refresh token after seven days. For long-running use, change the consent screen's publishing status to Production, delete the old token file, and authorize again. See Google's OAuth audience documentation for the applicable requirements.

Gmail(noauth_local_webserver=True) prints the authorization URL instead of opening it. Authorization still requires a local callback; Google no longer supports the old copy-and-paste flow.

Applications can inject existing google-auth credentials and skip file-based authentication:

import json
import os

from google.oauth2.credentials import Credentials
from simplegmail import Gmail

credentials = Credentials.from_authorized_user_info(
    json.loads(os.environ["GMAIL_TOKEN"])
)
gmail = Gmail(credentials=credentials)

GMAIL_TOKEN is an application-defined environment variable containing the authorized-user JSON normally stored in gmail_token.json.

Sending messages

Pass at least sender and to. A message may contain plain text, HTML, or both:

from simplegmail import Gmail

gmail = Gmail()
message = gmail.send_message(
    sender="me@example.com",
    to="you@example.com",
    subject="Hello",
    msg_plain="Hello from simplegmail.",
    msg_html="<p>Hello from <strong>simplegmail</strong>.</p>",
)

Add attachments, Cc, Bcc, or the configured Gmail signature as needed:

message = gmail.send_message(
    sender="Me <me@example.com>",
    to="you@example.com",
    cc=["copy@example.com"],
    bcc=["hidden@example.com"],
    subject="Report",
    msg_plain="The report is attached.",
    attachments=["reports/report.pdf", "images/chart.png"],
    signature=True,
)

Attachment bytes are preserved for all MIME types. The type is inferred from the filename and defaults to application/octet-stream when unknown.

Standard-library EmailMessage

For complete MIME control, construct an EmailMessage directly:

from email.message import EmailMessage

from simplegmail import Gmail

email = EmailMessage()
email["To"] = "you@example.com"
email["From"] = "me@example.com"
email["Subject"] = "Custom message"
email.set_content("Built with Python's email package.")

sent = Gmail().send_email_message(email)

Threaded replies

Use reply_to to send a response in the original Gmail thread:

original = gmail.get_messages(query='subject:"Original subject"')[0]

reply = gmail.send_message(
    sender="me@example.com",
    to=original.sender,
    msg_plain="Thanks for your message.",
    reply_to=original,
)

The original message must have a thread ID and Message-ID header. Its subject is reused because Gmail requires matching subjects when adding a reply to a thread.

Drafts

create_draft() accepts the same content and attachment options as send_message() and returns the Gmail API draft resource:

draft = gmail.create_draft(
    sender="me@example.com",
    to="you@example.com",
    subject="Work in progress",
    msg_plain="This message is not ready yet.",
)
print(draft["id"])

Retrieving messages

Convenience methods include get_unread_inbox(), get_starred_messages(), get_important_messages(), get_unread_messages(), get_drafts(), get_sent_messages(), get_trash_messages(), and get_spam_messages().

messages = gmail.get_unread_inbox()

for message in messages:
    print("To:", message.recipient)
    print("From:", message.sender)
    print("Subject:", message.subject)
    print("Date:", message.date)
    print("Preview:", message.snippet)
    print("Body:", message.plain or message.html or "")

For full control, use get_messages():

messages = gmail.get_messages(
    labels=["INBOX"],
    query="is:unread",
    attachments="reference",
    include_spam_trash=False,
    max_results=25,
)

The relevant options are:

Option Behavior
labels Requires every supplied Label object or label ID.
query Applies a Gmail search query.
attachments="ignore" Omits attachment metadata and data.
attachments="reference" Returns attachment metadata and downloads bytes only when requested. This is the default.
attachments="download" Downloads all attachment bytes while retrieving messages.
metadata_only=True Retrieves headers and size estimates without parsing bodies or attachments.
max_results=N Stops after at most N matching messages.
include_spam_trash=True Includes messages in spam and trash.
user_id Selects an account; the default "me" means the authenticated account.

attachments="download" retains every downloaded file in memory. Prefer the default reference mode for large mailboxes or when only selected files are needed.

Message.label_ids is a list of Gmail label ID strings. Use list_labels() when display names or Label objects are needed.

Labels and message state

labels = gmail.list_labels()
finance = next(item for item in labels if item.name == "Finance")

message = gmail.get_unread_inbox()[0]
message.mark_as_read()
message.star()
message.add_label(finance)
message.modify_labels(to_add="IMPORTANT", to_remove=finance)
message.archive()

Label mutation methods accept either Label objects or Gmail label ID strings. Other helpers include mark_as_unread(), unstar(), mark_as_spam(), mark_as_not_spam(), mark_as_important(), mark_as_not_important(), move_to_inbox(), trash(), and untrash().

Attachments

In the default reference mode, download() fetches bytes on demand and save() downloads if necessary before writing:

from pathlib import Path

Path("downloads").mkdir(exist_ok=True)
for message in gmail.get_unread_inbox():
    for attachment in message.attachments:
        if Path(attachment.filename).suffix.lower() == ".pdf":
            attachment.save(filepath="downloads")

If filepath is an existing directory, the stored filename is used safely within that directory. If it is a file path, that exact path is used. Existing files raise FileExistsError unless overwrite=True is passed.

The spec_attachment query term described below filters messages containing a matching attachment; it does not filter message.attachments after retrieval.

Search queries

construct_query() translates keyword arguments into Gmail search syntax. Tuples join values with AND; lists join values with OR. The labels term is special: a flat list requires all labels, while a nested list expresses alternatives.

from datetime import datetime, timedelta, timezone

from simplegmail.query import construct_query

query = construct_query(
    newer_than=(2, "day"),
    unread=True,
    labels=[["Finance"], ["Homework", "CS"]],
)
messages = gmail.get_messages(query=query, max_results=10)

ten_minutes_ago = datetime.now(timezone.utc) - timedelta(minutes=10)
recent = gmail.get_messages(
    query=construct_query(after=int(ten_minutes_ago.timestamp()))
)

Prefix a keyword with exclude_ or pass False to negate a boolean term:

query = construct_query(unread=True, exclude_starred=True)

Pass multiple dictionaries to OR complete queries:

query = construct_query(
    {"sender": "alerts@example.com", "newer_than": (2, "day")},
    {"labels": ["Top Secret"], "starred": False},
)
messages = gmail.get_messages(query=query)

Do not mix query dictionaries and keyword terms in one call. Empty sequence values and unknown terms raise ValueError. See construct_query() in simplegmail/query.py for the complete keyword list.

Errors and resource cleanup

Google API request failures propagate as googleapiclient.errors.HttpError. Invalid local arguments raise ValueError, and inconsistent label mutation responses raise RuntimeError.

For a long-lived process, reuse one Gmail instance. When finished, its underlying HTTP service can be closed explicitly:

gmail.service.close()

Upgrading to 5.0

  • Python 3.10 or newer is required.
  • Authentication uses google-auth; legacy oauth2client token files with a refresh token are migrated when refreshed.
  • Browser authorization uses a local callback server; the retired manual copy-and-paste flow is unavailable.
  • Message.label_ids consistently contains Gmail label ID strings.

Development

python -m pip install -e '.[test]'
python -m pytest

Report bugs and request features through GitHub Issues.

Metadata

Release files for simplegmail 5.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for simplegmail 5.0.0
File Size Uploaded
simplegmail-5.0.0.tar.gz 33.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for simplegmail 5.0.0
File Interpreter ABI Platform
simplegmail-5.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 57.2 kB

Release files / simplegmail-5.0.0.tar.gz

Download URL simplegmail-5.0.0.tar.gz
Size 33.7 kB
Tags Source
SHA-256 checksum
How to use checksums
92b7e2cc48f40b4727214cd9fe46a6490df8e5d9dffcc9f8d636f1935408d338
BLAKE2b-256 checksum
How to use checksums
923507563611aab0d5f60b7eb022b65b0c6ec43ba24f2f79d45d7185fb90d723
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.15

Release files / simplegmail-5.0.0-py3-none-any.whl

Download URL simplegmail-5.0.0-py3-none-any.whl
Size 23.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ca9bcb7556aa13d09114c4386e7f38a026759c8a0bab3649a4688ec1ebfdd98c
BLAKE2b-256 checksum
How to use checksums
e38b499f58c1fa07178f53c451684e7d1e3f73813d10ac2c783ab9bb15dedf58
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.15

Release history Release notifications | RSS feed

This release

5.0.0 This release

2 release files

4.1.1

2 release files

4.1.0

2 release files

4.0.4

2 release files

4.0.3

2 release files

4.0.2

2 release files

4.0.1

2 release files

4.0.0

2 release files

3.1.6

2 release files

3.1.5

2 release files

3.1.4

2 release files

3.1.3

2 release files

3.1.2

2 release files

3.1.1

2 release files

3.1.0

2 release files

3.0.0

2 release files

2.1.0

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.0.0

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page