simplegmail
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
EmailMessageobjects, 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:
- Create or select a project in the Google Cloud console.
- Enable the Gmail API.
- Configure the OAuth consent screen.
- Create an OAuth client ID with the application type Desktop app.
- Download the client JSON as
client_secret.jsoninto 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; legacyoauth2clienttoken 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_idsconsistently contains Gmail label ID strings.
Development
python -m pip install -e '.[test]'
python -m pytest
Report bugs and request features through GitHub Issues.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file simplegmail-5.0.0.tar.gz.
File metadata
- Download URL: simplegmail-5.0.0.tar.gz
- Upload date:
- Size: 33.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92b7e2cc48f40b4727214cd9fe46a6490df8e5d9dffcc9f8d636f1935408d338
|
|
| MD5 |
985746f195df0447dc27e25055743b4f
|
|
| BLAKE2b-256 |
923507563611aab0d5f60b7eb022b65b0c6ec43ba24f2f79d45d7185fb90d723
|
File details
Details for the file simplegmail-5.0.0-py3-none-any.whl.
File metadata
- Download URL: simplegmail-5.0.0-py3-none-any.whl
- Upload date:
- Size: 23.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ca9bcb7556aa13d09114c4386e7f38a026759c8a0bab3649a4688ec1ebfdd98c
|
|
| MD5 |
445126f902e841db722a1cd94aa5b175
|
|
| BLAKE2b-256 |
e38b499f58c1fa07178f53c451684e7d1e3f73813d10ac2c783ab9bb15dedf58
|