users.messages.list
to find message IDs and users.messages.get
(with format=metadata
) to fetch just the Subject header efficiently. Google for Developers+2Google for Developers+2credentials.json
. Google for Developerscredentials.json
in your project folder. The first run will open a browser to grant access and store a token.json
locally (so you won’t have to log in again). Google for DevelopersWe’ll request the minimal read-only scope:
https://www.googleapis.com/auth/gmail.readonly
. Google for Developers
python -m venv venv
# Windows: venv\Scripts\activate
source venv/bin/activate
pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib
These are the official client libraries used in Google’s quickstart. Google for Developers
asyncio
using asyncio.to_thread(...)
so the event loop stays responsive.await asyncio.sleep(N)
.seen_ids
set so we only print new messages.# gmail_async_poll.py
import asyncio
import os
from typing import Set, List, Dict
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
# ---- OAuth scope: read-only inbox access ----
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
def get_service():
"""
Synchronous: builds an authenticated Gmail API service.
Based on the official quickstart pattern.
"""
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:
# refresh silently
from google.auth.transport.requests import Request
creds.refresh(Request())
else:
# first-time browser OAuth flow
flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
creds = flow.run_local_server(port=0)
# save token for next runs
with open("token.json", "w") as f:
f.write(creds.to_json())
return build("gmail", "v1", credentials=creds)
def list_message_ids_sync(service, q: str, label_ids: List[str], max_results: int = 20) -> List[str]:
"""
Synchronous: returns a list of recent message IDs using users.messages.list.
We use a Gmail search query 'q' and optional label filters.
"""
res = service.users().messages().list(
userId="me", q=q, labelIds=label_ids, maxResults=max_results
).execute()
msgs = res.get("messages", [])
return [m["id"] for m in msgs]
def get_subject_sync(service, msg_id: str) -> Dict[str, str]:
"""
Synchronous: fetch only metadata headers for fast access,
then extract Subject (and a few extras).
"""
res = service.users().messages().get(
userId="me",
id=msg_id,
format="metadata",
metadataHeaders=["Subject", "From", "Date"],
).execute()
headers = {h["name"]: h["value"] for h in res.get("payload", {}).get("headers", [])}
return {"id": msg_id, "Subject": headers.get("Subject", "(no subject)"),
"From": headers.get("From", ""), "Date": headers.get("Date", "")}
async def poll_subjects(interval_seconds: int = 10,
query: str = "is:unread",
labels: List[str] = ["INBOX"]):
"""
Async polling loop:
- every `interval_seconds`:
- list recent messages matching query/labels
- for any new IDs, fetch Subject via metadata and print it
"""
print(f"Starting async poll every {interval_seconds}s for query={query!r}, labels={labels}")
# Build the Gmail service once (sync), then reuse it.
service = await asyncio.to_thread(get_service)
seen: Set[str] = set()
while True:
try:
# 1) list IDs (run sync call in a thread to avoid blocking)
ids = await asyncio.to_thread(list_message_ids_sync, service, query, labels, 20)
# 2) for any new IDs, fetch metadata concurrently
new_ids = [i for i in ids if i not in seen]
if new_ids:
tasks = [asyncio.to_thread(get_subject_sync, service, mid) for mid in new_ids]
results = await asyncio.gather(*tasks)
for r in results:
print(f"[NEW] {r['Date']} {r['From']} :: {r['Subject']}")
seen.add(r["id"])
# 3) sleep and repeat
await asyncio.sleep(interval_seconds)
except KeyboardInterrupt:
print("Stopping poller...")
break
if __name__ == "__main__":
# Change the interval or query if you like:
# Examples for q:
# - "is:unread"
# - "newer_than:1d"
# - "label:unread from:github"
# (Note: some queries require scopes beyond gmail.metadata; we use gmail.readonly.)
asyncio.run(poll_subjects(interval_seconds=8, query="is:unread", labels=["INBOX"]))
get_service()
: Implements the OAuth flow from Google’s quickstart—loads saved tokens if present, runs a browser consent if not, then builds a Gmail API client. Google for Developerslist_message_ids_sync()
: Calls users.messages.list
with a Gmail search query (q
) and optional label filters; only returns message IDs (that’s how the API works). Google for DevelopersStack Overflowget_subject_sync()
: Calls users.messages.get
with format=metadata
and metadataHeaders=Subject,From,Date
to efficiently pull only headers (no full body). Google for Developers+1googleapis.github.iopoll_subjects()
: An async loop:
await asyncio.to_thread(...)
to run each synchronous API call in a thread (so the event loop stays free).asyncio.gather
.interval_seconds
with await asyncio.sleep(...)
and repeats.python gmail_async_poll.py
token.json
is saved. Google for Developersis:unread
in INBOX), you’ll see:[NEW] Tue, 12 Aug 2025 18:03:22 +0000 Sender Name <sender@example.com> :: Welcome to Async Gmail!
from:github newer_than:1d label:unread
. (Be mindful: certain queries require appropriate scopes; gmail.metadata
has restrictions, we’re using gmail.readonly
.) Google Hilfeseen
set. For reliability across restarts, persist it (e.g., a small SQLite DB or a file) or track Gmail history IDs (advanced). Google for Developersinterval_seconds
(e.g., 15–60s) for production.format=metadata
is lighter/faster than fetching full messages. Google for Developerswatch
/PubSub, which is more scalable—but polling is simpler for a quick tool. (See the Gmail API guides if you want to evolve this.) Google for Developersasyncio.to_thread
The Google API client is synchronous; wrapping calls with to_thread
keeps your app responsive and lets you concurrently fetch multiple subjects without blocking the main loop.