Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Unsub

A personal Gmail subscription unsubscriber. It scans your inbox for the List-Unsubscribe / List-Unsubscribe-Post headers (RFC 2369 / RFC 8058) that mailing senders attach to messages, groups matches by sender, and classifies each one so you can unsubscribe with as few clicks as possible wherever the sender's own headers actually support it:

  • One-click — sender supports RFC 8058's single-POST unsubscribe, no redirects.
  • Mailto — sender expects an unsubscribe email; sent for you via the Gmail API.
  • Link only — sender only offers a web page; you'll need to open it yourself.
  • Not supported — no List-Unsubscribe header at all; deprioritized for v1.

Runs entirely locally, against your own Gmail account, using your own Google Cloud OAuth credentials — bring your own client secrets, nothing is hosted centrally.

Why gmail.metadata + gmail.send

The app requests two OAuth scopes:

  • gmail.metadata — headers only, never message bodies. This is what scanning uses, and it's the reason scans can't do server-side date filtering (see below).
  • gmail.send — narrowly needed to send the unsubscribe email for mailto senders. It can send mail as you but still cannot read any message content.

Setup

1. Google Cloud OAuth credentials (manual, one-time)

  1. Create a project in Google Cloud Console.

  2. Enable the Gmail API for it.

  3. Configure the OAuth consent screen in Testing mode and add your own Google account as a test user — this avoids Google's app-verification review, since this is a single-user personal tool.

  4. Create an OAuth client of type Desktop app, download the JSON, and save it as credentials/client_secret.json.

    Note: in Testing mode, refresh tokens can expire after ~7 days of inactivity. If the app later fails to start complaining about an invalid token, just re-run step 2 below.

2. Authorize the app

python scripts/authorize.py

Opens your browser for Google's consent screen once, then writes credentials/token.json. Neither credentials file is ever committed (see .gitignore).

3. Postgres

Native install, no Docker:

brew install postgresql@14
brew services start postgresql@14
createuser unsub
createdb unsub -O unsub

postgresql@14 is deprecated upstream and Homebrew will disable the formula around 2026-11-12 — fine for now, but worth moving to a newer major version (e.g. postgresql@16) before then. Any 14.x+ install works identically for this app; nothing here is version-specific.

Copy .env.example to .env and adjust DATABASE_URL if needed.

4. Install and run

python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
uvicorn app.main:app --reload

The app creates its own tables on startup (Base.metadata.create_all() — no migration tool; see the plan notes if you want to add Alembic later). Visit http://localhost:8000.

Testing

pytest

test_classify.py/test_grouping.py/test_gmail_scanner.py are pure/mocked and always run. test_scan_service.py/test_unsubscribe_service.py need a real Postgres reachable at TEST_DATABASE_URL (defaults to postgresql+psycopg://unsub@localhost:5432/unsub_test) and skip cleanly if one isn't available.

A known performance tradeoff

The gmail.metadata scope forbids the q search parameter on messages.list, and messages.list carries no date information at all — only messages.get returns a message's internalDate. So a scan's date-range filter (default: past 3 months, configurable in the UI) doesn't reduce how many messages get fetched — it only changes which fetched messages get kept. Every scan walks the full INBOX message list. This is simple and always correct, but on a mailbox with years of history, the first scan may take a while.

About

Unsubscribe from gmail email subscriptions

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages