Unread Herring
“Cut down the mightiest inbox.”
A local-first, open-source Elixir tool that scans your Gmail, renders an interactive sunburst of where your mail comes from (grouped by sender domain, sender, or label), lets you drill down through the rings, and lets you click a wedge to open that exact filtered view in Gmail. Built on Phoenix LiveView; launched from the terminal.
Nothing leaves your machine except calls to the Gmail API itself. No database, no hosted service, no telemetry, no shipped credentials.
This tool bulk-modifies your real mailbox. By using it you accept that you do so entirely at your own risk and that you must know what you are doing:
gmail.modify scope.is:unread searches will no
longer find them.from: search matches more broadly than the chart’s exact-domain
grouping (subdomains, plus-addressing), so an action can affect somewhat
more mail than the wedge count suggests. The confirm dialog says so.If any of the above gives you pause, explore the chart and the “Open in Gmail” links only, and leave the Mark read button alone.
mix herring.serve boots an OTP app with a Phoenix endpoint bound to
127.0.0.1 and opens your browser.~/.config/unread_herring/token.json with 0600 permissions.This project requires Google credentials to work. You create your own OAuth client in your own Google Cloud project, so your data is only ever between you and Google:
unread-herring).~/.config/unread_herring/credentials.json, orexport environment variables:
export GOOGLE_CLIENT_ID="...apps.googleusercontent.com"
export GOOGLE_CLIENT_SECRET="..."
Note: while the consent screen is in Testing mode, Google may expire refresh tokens after about 7 days; just re-run the auth flow when prompted.
mix deps.get
mix herring.serve # boots on http://127.0.0.1:4000 and opens your browser
Useful extras:
mix herring.smoke # prints sender-domain counts to stdout (API smoke test)
mix test # full test suite; no live Gmail calls anywhere
Scan cap. A scan fetches at most 10,000 messages by default (newest first). Each scanned message costs one Gmail metadata request, and Gmail’s per-user quota works out to roughly 50 requests/second, so 10,000 messages take a few minutes. When a scan hits the cap the dashboard shows a warning, since the chart then only reflects the most recent slice. Adjust the cap per scan with the “Max messages” box in the dashboard controls (up to 100,000), or change the default it starts with:
HERRING_SCAN_MAX=50000 mix herring.serve
Rate limiting. Google enforces a per-minute quota on top of the per-second one. Large scans can hit it; the app backs off and retries automatically, and if messages still could not be fetched the dashboard shows a warning that the chart is incomplete - wait a minute and scan again.
Bulk mark-read applies to at most 10,000 matching messages per click; the toast says “at least N” when there may be more, and clicking again continues where it left off.
gmail.modify, and the only write the code ever
performs is removing the UNREAD label (mark read). There is no archive,
trash or delete code path.~/.config/unread_herring/ (OAuth token,
0600).Build and run a standard self-contained release:
mix assets.deploy # compile + digest assets (prod requires the manifest)
MIX_ENV=prod mix release
PHX_SERVER=1 PORT=4000 _build/prod/rel/unread_herring/bin/unread_herring start
Note: mix release builds for the current MIX_ENV, so a bare invocation
produces a dev-mode release under _build/dev/rel/... instead - set
MIX_ENV=prod for the real thing.
Standard Phoenix app, no Ecto. See unread-herring-plan.md for the full
design document and CLAUDE.md for a condensed architecture overview.
See LICENSE.