You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
oabrivard 42189e972d Added docker config 7 days ago
app Fixed PyLance type errors 7 days ago
typings Fixed PyLance type errors 7 days ago
.DS_Store Debugged gmail access code 1 week ago
.dockerignore Added docker config 7 days ago
.env.example Debugged gmail access code 1 week ago
.gitignore Added unsubscribe email recap 1 week ago
Dockerfile Added docker config 7 days ago
README.md Added docker config 7 days ago
docker-compose.yml Added docker config 7 days ago
pyproject.toml Updated env variables 1 week ago
simple-agent-google-credentials.json Updated env variables 1 week ago
uv.lock Updated env variables 1 week ago

README.md

Personal Gmail + Calendar Agent

This project runs a small local API service that:

  • scans unread emails in the root Gmail inbox
  • classifies emails with Strands (LINKEDIN, ADVERTISING, VEILLE_TECHNO, OTHER)
  • moves LinkedIn emails to a LinkedIn label/folder
  • moves advertising emails to an Advertising label/folder
  • moves veille techno emails to a VeilleTechno label/folder
  • scans the Advertising label and emails you new unsubscribe links (deduplicated)
  • discovers unsubscribe-ready mailing lists for human review, then auto-unsubscribes selected lists
  • exposes a secure availability endpoint powered by Google Calendar free/busy

1) Prerequisites

  • Python 3.11+
  • uv (installation guide)
  • A Google account
  • An OpenAI-compatible API key for Strands (STRANDS_OPENAI_API_KEY)
  • A Google Cloud project with:
    • Gmail API enabled
    • Google Calendar API enabled
    • OAuth consent configured
    • OAuth Client ID of type Desktop app

2) Google OAuth setup

  1. In Google Cloud Console, create a desktop OAuth client.
  2. Download the client JSON file.
  3. Save it in this project as credentials.json.

The first run opens a browser window for consent and creates token.json. If your existing token was created before gmail.send was added, you may be prompted again.

3) Install and configure

uv sync
cp .env.example .env

Edit .env and set:

  • AGENT_API_KEY to a strong secret for agent-to-agent calls
  • STRANDS_OPENAI_API_KEY and optional STRANDS_MODEL_ID / STRANDS_OPENAI_BASE_URL
  • optional unsubscribe digest settings (UNSUBSCRIBE_*)
  • optional scan frequency and additional Gmail filters (GMAIL_QUERY)

4) Run

Local

uv run uvicorn app.main:app --reload

Docker

docker compose up --build -d
docker compose logs -f

Container mounts:

  • ./${GOOGLE_CLIENT_SECRETS_FILE} -> /app/${GOOGLE_CLIENT_SECRETS_FILE} (read-only)
  • ./${GOOGLE_TOKEN_FILE} -> /app/${GOOGLE_TOKEN_FILE}
  • ./data -> /app/data

At startup, the scheduler runs:

  • Gmail triage (GMAIL_SCAN_INTERVAL_MINUTES)
  • unsubscribe digest (UNSUBSCRIBE_DIGEST_INTERVAL_MINUTES)
  • auto-unsubscribe for approved lists (UNSUBSCRIBE_AUTO_INTERVAL_MINUTES) when UNSUBSCRIBE_AUTO_ENABLED=true

5) API usage

Health check

curl http://127.0.0.1:8000/health

Manual Gmail scan

curl -X POST "http://127.0.0.1:8000/scan?max_results=100" \
  -H "X-API-Key: your-secret"

Availability for other AI agents

curl -X POST "http://127.0.0.1:8000/availability" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret" \
  -d '{
    "start": "2026-03-09T09:00:00+01:00",
    "end": "2026-03-09T10:00:00+01:00",
    "calendar_ids": ["primary"]
  }'

If available is true, there are no busy slots in that range.

Manual unsubscribe digest

curl -X POST "http://127.0.0.1:8000/unsubscribe-digest?max_results=500" \
  -H "X-API-Key: your-secret"

Human-in-the-loop unsubscribe candidates

curl -X POST "http://127.0.0.1:8000/unsubscribe/candidates?max_results=500" \
  -H "X-API-Key: your-secret"

Execute unsubscribe for selected mailing lists

curl -X POST "http://127.0.0.1:8000/unsubscribe/execute" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret" \
  -d '{
    "selected_candidate_ids": ["abc123def456", "987zyx654wvu"],
    "remember_selection": true
  }'

Trigger auto-unsubscribe run (approved lists only)

curl -X POST "http://127.0.0.1:8000/unsubscribe/auto-run?max_results=500" \
  -H "X-API-Key: your-secret"

Classification behavior

  • Scan scope is always forced to in:inbox is:unread (root inbox + unread).
  • GMAIL_QUERY is treated as additional filters (for example -label:AgentProcessed).
  • Strands classification is used for each email (LINKEDIN, ADVERTISING, VEILLE_TECHNO, OTHER).
  • LinkedIn has priority over advertising inside the classifier prompt.
  • Set LLM_FALLBACK_TO_RULES=true only if you want rules-based backup when LLM calls fail.
  • Every scanned message gets an AgentProcessed label to avoid reprocessing loops.

Unsubscribe digest behavior

  • Reads emails from UNSUBSCRIBE_QUERY (default label:Advertising).
  • Extracts unsubscribe URLs from List-Unsubscribe headers and message content.
  • Removes duplicates within the run and across runs.
  • Persists already sent links in UNSUBSCRIBE_STATE_FILE.
  • Sends only new links by email, unless UNSUBSCRIBE_SEND_EMPTY_DIGEST=true.

Human-In-The-Loop Unsubscribe

  • Candidate discovery groups advertising messages into mailing lists using headers (List-Id, From) and unsubscribe methods.
  • You pick the candidate_id values you want to unsubscribe from.
  • Only selected lists are executed.
  • Executed unsubscribe methods are persisted and never executed twice.
  • Selected lists can be remembered (remember_selection=true) for scheduled auto-unsubscribe.
  • State is saved in UNSUBSCRIBE_HIL_STATE_FILE.

Notes

  • Gmail "folders" are labels. This agent creates:
    • LinkedIn
    • VeilleTechno
    • Advertising
    • AgentProcessed
  • Messages classified as LinkedIn/Advertising/Veille_Techno are removed from INBOX (moved out of inbox).