Skip to content

Instantly share code, notes, and snippets.

@jrork
Created January 27, 2026 01:30
Show Gist options
  • Select an option

  • Save jrork/c2e37e7bb3fd0e7a72041bc846feeb94 to your computer and use it in GitHub Desktop.

Select an option

Save jrork/c2e37e7bb3fd0e7a72041bc846feeb94 to your computer and use it in GitHub Desktop.
How I Connected Clawdbot to Gmail Without Opening Any Ports

Real-Time Gmail Notifications Without Exposing Your Server

TL;DR: I set up instant Gmail notifications to my AI assistant using Google Pub/Sub's streaming pull — no webhooks, no exposed ports, no ngrok. Just an outbound gRPC connection that receives push notifications in real-time.

The Problem

I wanted my AI assistant (Clawdbot, running on a Raspberry Pi 5) to know when I get important emails. The obvious solutions all had problems:

  • Polling Gmail API — Works, but 5-minute latency feels archaic
  • Gmail Pub/Sub with push webhooks — Requires exposing a port to the internet. No thanks.
  • Tailscale Funnel / ngrok — Still exposes a service. Attack surface I don't want.

The Solution: Streaming Pull

Google Pub/Sub supports two subscription types:

  1. Push — Google calls YOUR webhook (requires exposed port)
  2. Pull — YOU call Google's API to fetch messages

But here's what most people miss: Pull isn't just polling. Pub/Sub supports streaming pull via gRPC — your client opens an outbound connection and holds it open. Google sends messages through that connection in real-time.

No exposed ports. No inbound connections. Sub-second latency.

Security Model

This was the key insight: separate credentials with minimal scope.

Component Credentials Permissions
Pub/Sub daemon Service account Can ONLY pull from one subscription
Email fetching OAuth token Gmail read-only
Clawdbot webhook Localhost only No external access

If the Pi is compromised:

  • The service account can't read emails (only knows "something arrived")
  • The OAuth token can read but not send emails
  • Neither can access other GCP resources

Setup Steps

1. Create GCP Resources

# Create Pub/Sub topic
gcloud pubsub topics create gmail-watch

# Allow Gmail to publish
gcloud pubsub topics add-iam-policy-binding gmail-watch \
  --member="serviceAccount:gmail-api-push@system.gserviceaccount.com" \
  --role="roles/pubsub.publisher"

# Create pull subscription
gcloud pubsub subscriptions create gmail-watch-pull \
  --topic=gmail-watch

# Create minimal service account
gcloud iam service-accounts create gmail-pubsub-reader

# Grant ONLY subscriber permission on this ONE subscription
gcloud pubsub subscriptions add-iam-policy-binding gmail-watch-pull \
  --member="serviceAccount:gmail-pubsub-reader@PROJECT.iam.gserviceaccount.com" \
  --role="roles/pubsub.subscriber"

2. Start Gmail Watch

# Set up Gmail watch (requires OAuth token with Gmail scope)
# This tells Gmail to publish notifications to your Pub/Sub topic
curl -X POST \
  'https://gmail.googleapis.com/gmail/v1/users/me/watch' \
  -H 'Authorization: Bearer YOUR_OAUTH_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "topicName": "projects/PROJECT/topics/gmail-watch",
    "labelIds": ["INBOX"]
  }'

3. Python Daemon

from google.cloud import pubsub_v1
import json

def callback(message):
    data = json.loads(message.data.decode())
    history_id = data.get("historyId")
    
    # Gmail only sends "something changed" — fetch actual content separately
    emails = fetch_emails_via_gmail_api(history_id)
    
    for email in emails:
        notify_my_assistant(email)
    
    message.ack()

subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path("PROJECT", "gmail-watch-pull")
subscriber.subscribe(subscription_path, callback=callback)

# Keep the main thread alive
import time
while True:
    time.sleep(60)

The subscribe() call opens a streaming gRPC connection. It blocks forever, calling your callback when messages arrive.

4. Systemd Service

[Unit]
Description=Gmail Pub/Sub Watch Daemon
After=network-online.target

[Service]
Type=simple
ExecStart=/path/to/venv/bin/python /path/to/gmail_watch.py
Restart=always
RestartSec=10

[Install]
WantedBy=default.target

Gotchas

  1. Gmail watch expires after 7 days — Need a cron job to renew
  2. Pub/Sub may deliver duplicates — Track seen message IDs
  3. Gmail only sends historyId — You still need to call Gmail API for content
  4. Multiple notifications per email — Deduplicate by message ID

The Result

  • Latency: ~2-3 seconds from email arrival to notification
  • Security: No exposed ports, minimal credential scope
  • Reliability: Systemd restarts on failure, gRPC reconnects automatically
  • Cost: Free tier covers personal email easily

Why This Matters

The conventional wisdom is "you need a webhook for real-time notifications." But streaming pull gives you the same latency with a fundamentally better security model — your server never accepts inbound connections.

This pattern works for any Pub/Sub use case, not just Gmail. If you're building something that needs real-time events but you don't want to expose a webhook, streaming pull is the answer.


Running on a Raspberry Pi 5 with Clawdbot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment