PythonintermediateRecipeUpdated 20 Jan 2025
Gmail label to Slack channel
When a Gmail message hits a specific label, post a compact summary into a Slack channel — including sender, subject, and a deep link.
#gmail#slack#notifications#oauth
The problem
Your team triages a shared support@ inbox. When a message is labelled
urgent, you want it surfaced in #support-urgent on Slack within a
minute — with the original still available in Gmail so nothing gets
lost.
This recipe polls Gmail every 60 seconds, picks up newly-labelled messages, and posts them to an incoming Slack webhook. It remembers which message IDs it has already posted in a tiny JSON file so restarts are safe.
Install
pip install google-api-python-client google-auth google-auth-oauthlib requests
Set two environment variables:
SLACK_WEBHOOK_URL— an Incoming Webhook from SlackGMAIL_CREDENTIALS_JSON— path to your OAuth client secrets file
The script
import json
import os
import time
from pathlib import Path
import requests
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"]
STATE_FILE = Path("seen.json")
LABEL_NAME = "urgent"
POLL_SECONDS = 60
SLACK_URL = os.environ["SLACK_WEBHOOK_URL"]
def gmail_service():
token = Path("token.json")
if token.exists():
creds = Credentials.from_authorized_user_file(str(token), SCOPES)
else:
flow = InstalledAppFlow.from_client_secrets_file(
os.environ["GMAIL_CREDENTIALS_JSON"], SCOPES
)
creds = flow.run_local_server(port=0)
token.write_text(creds.to_json())
return build("gmail", "v1", credentials=creds)
def load_seen() -> set[str]:
if STATE_FILE.exists():
return set(json.loads(STATE_FILE.read_text()))
return set()
def save_seen(seen: set[str]) -> None:
STATE_FILE.write_text(json.dumps(sorted(seen)))
def label_id(svc, name: str) -> str:
for lbl in svc.users().labels().list(userId="me").execute().get("labels", []):
if lbl["name"].lower() == name.lower():
return lbl["id"]
raise RuntimeError(f"label {name!r} not found")
def header(msg, name: str) -> str:
for h in msg["payload"]["headers"]:
if h["name"].lower() == name.lower():
return h["value"]
return ""
def post_to_slack(msg) -> None:
subject = header(msg, "Subject") or "(no subject)"
sender = header(msg, "From") or "(unknown)"
url = f"https://mail.google.com/mail/u/0/#all/{msg['id']}"
payload = {
"text": f":rotating_light: *{subject}*\nfrom `{sender}`\n<{url}|Open in Gmail>",
}
r = requests.post(SLACK_URL, json=payload, timeout=10)
r.raise_for_status()
def main():
svc = gmail_service()
lid = label_id(svc, LABEL_NAME)
seen = load_seen()
print(f"watching label {LABEL_NAME!r} — known: {len(seen)}")
while True:
try:
resp = svc.users().messages().list(
userId="me", labelIds=[lid], maxResults=25,
).execute()
for meta in resp.get("messages", []):
mid = meta["id"]
if mid in seen:
continue
full = svc.users().messages().get(userId="me", id=mid, format="metadata").execute()
post_to_slack(full)
seen.add(mid)
save_seen(seen)
print(f" posted {mid}")
except Exception as exc:
print(f"!! poll error: {exc}")
time.sleep(POLL_SECONDS)
if __name__ == "__main__":
main()
Security notes
- Use a service account with domain-wide delegation if you need to read a shared mailbox rather than your own. Personal OAuth tokens are fine for single-user triage.
- The Slack webhook URL is a credential — store it in a secret manager, not in git.
- This is read-only (
gmail.readonly). If you want to auto-archive after posting, upgrade togmail.modifyand callmessages.modifyto remove the label.
Extensions
- Parse the body and attach a 300-char preview.
- Route by keywords:
#urgent-financevs#urgent-support. - Move from polling to Gmail’s push notifications via Pub/Sub for sub-second latency.