{"id":50973815,"url":"https://github.com/bradbarrish/unimportant-gmail","last_synced_at":"2026-06-19T05:32:59.179Z","repository":{"id":362216743,"uuid":"1047654534","full_name":"bradbarrish/unimportant-gmail","owner":"bradbarrish","description":"A learning loop for keeping garbage out of your Gmail inbox","archived":false,"fork":false,"pushed_at":"2026-06-03T05:56:45.000Z","size":25,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-03T07:30:13.846Z","etag":null,"topics":["filtering","gmail","productivity"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bradbarrish.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-30T22:57:40.000Z","updated_at":"2026-06-03T05:56:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bradbarrish/unimportant-gmail","commit_stats":null,"previous_names":["bradbarrish/unimportant-gmail"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/bradbarrish/unimportant-gmail","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bradbarrish%2Funimportant-gmail","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bradbarrish%2Funimportant-gmail/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bradbarrish%2Funimportant-gmail/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bradbarrish%2Funimportant-gmail/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bradbarrish","download_url":"https://codeload.github.com/bradbarrish/unimportant-gmail/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bradbarrish%2Funimportant-gmail/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34519049,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-19T02:00:06.005Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["filtering","gmail","productivity"],"created_at":"2026-06-19T05:32:57.873Z","updated_at":"2026-06-19T05:32:59.173Z","avatar_url":"https://github.com/bradbarrish.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Unimportant Gmail\n\nA learning loop for keeping your Gmail inbox personal-only.\n\nIf you label noise — either with a manual click or via a catch-all filter — this script turns that label into a permanent Gmail filter. The next time the same sender shows up, Gmail handles it before you ever see it.\n\n## The idea\n\nMost inboxes are ~98% bulk: newsletters, receipts, marketing, notifications, automated mail. If you want only personal email in your inbox, you have to filter aggressively. Doing that by hand at scale is exhausting. This script automates the loop:\n\n1. You designate a label as \"this should never reach my inbox\" — by default `↓ Unimportant`.\n2. Anything that lands with that label (whether you applied it manually, or a catch-all filter did) gets noticed.\n3. The script reads your existing filters and figures out which of those senders are not yet filtered.\n4. With `--apply`, it creates a Gmail filter for each missing sender so it skips the inbox in future.\n\nOver time the inbox quiets down, and the only things still landing in it are senders you genuinely want to hear from.\n\n## What the script does\n\n1. Authenticates to Gmail via OAuth (read messages + manage filters scopes).\n2. Pulls every message under the chosen label in the last 14 days and dedupes the senders.\n3. Lists your existing filters and extracts the addresses + domains they target.\n4. Diffs (2) against (3). Anything not already covered is a candidate.\n5. Writes a report and two CSVs (`senders.csv`, `diff.csv`). With `--apply`, creates a new Gmail filter for each candidate that applies the label and removes `INBOX`.\n\n## Prerequisites\n\n- Python 3.10 or newer\n- A Google account with the Gmail you want to manage\n- The label you want to filter into already exists in your Gmail (default: `↓ Unimportant`)\n\n## One-time Google Cloud setup\n\nGmail requires an OAuth client. This is free and takes about five minutes.\n\n### 1. Create or select a project\n\nGo to \u003chttps://console.cloud.google.com\u003e. Use any existing project, or click the project picker in the top bar and choose **New Project**. Name it whatever you want.\n\n### 2. Enable the Gmail API\n\nOpen \u003chttps://console.cloud.google.com/apis/library/gmail.googleapis.com\u003e, make sure your project is selected, and click **Enable**.\n\n### 3. Configure the OAuth consent screen\n\nOpen \u003chttps://console.cloud.google.com/apis/credentials/consent\u003e.\n\n- User type: **External**\n- App name: anything (e.g. `unimportant-gmail`)\n- User support email: your own\n- Developer contact: your own\n- Scopes: leave empty — the script declares them at runtime\n- Test users: **add the Google account whose Gmail you want to filter**. Without this step you'll hit \"access blocked\" during OAuth.\n\nYou can leave the app in **Testing** mode. You don't need to publish it.\n\n### 4. Create OAuth client credentials\n\nOpen \u003chttps://console.cloud.google.com/apis/credentials\u003e, then **Create Credentials → OAuth client ID**.\n\n- Application type: **Desktop app**\n- Name: anything (e.g. `unimportant-gmail CLI`)\n- Click **Create**, then **Download JSON**\n\n### 5. Save the JSON\n\nMove the downloaded file into this directory and rename it to `credentials.json`. It is git-ignored.\n\n## Install\n\n```sh\npython3 -m venv .venv\n.venv/bin/pip install -r requirements.txt\n```\n\n## First run\n\n```sh\n.venv/bin/python unimportant_gmail.py\n```\n\nThis is a dry run. The first time, your browser will pop open for OAuth — pick the Google account you added as a test user, click through the \"Google hasn't verified this app\" warning (it's your own app), and grant the scopes. A refresh token will be cached to `token.json` so subsequent runs are non-interactive.\n\nYou'll get a report showing how many candidate senders the script found. Look at `senders.csv` and `diff.csv` to sanity-check.\n\nWhen you're ready to actually create the filters:\n\n```sh\n.venv/bin/python unimportant_gmail.py --apply\n```\n\nYou can re-run any time — the script is idempotent. It only creates filters for senders it can't find an existing match for.\n\n## CLI options\n\n```text\n--label LABEL          Label name to scan. Default: '↓ Unimportant'\n--window WINDOW        Gmail search window. Default: 'newer_than:14d'\n--credentials PATH     OAuth client JSON. Default: ./credentials.json\n--token PATH           Cached token JSON. Default: ./token.json\n--output-dir DIR       Where senders.csv and diff.csv land. Default: script dir\n--apply                Actually create filters (default is dry-run).\n--no-auth-flow         Exit with an error instead of opening a browser if interactive\n                       auth is needed. Use this in cron.\n```\n\nThe filter action — `apply LABEL + remove INBOX` — is hardcoded. If you want different behavior (delete on arrival, archive without label, apply a different label), edit the `create_filter` function. It's a one-line dict.\n\n## Tests\n\n```sh\n.venv/bin/python test_unimportant_gmail.py\n```\n\nThe tests cover the filter-criterion parser and the sender-matching logic. These are the parts of the code where a subtle bug can cause the script to silently skip filters that should be created. If you touch `parse_from_criterion` or `is_sender_filtered`, run the tests.\n\n## How sender matching works\n\nThe script parses every existing filter's `from:` criterion into two sets:\n\n- **Exact emails** — addresses where the local part is significant (e.g. `billing@example.com`).\n- **Domains** — domains where any sender at that domain is covered (e.g. `example.com`, written as `@example.com` or bare `example.com` in the filter).\n\nA sender from your `↓ Unimportant` label counts as \"already filtered\" if its exact address is in the exact-email set, **or** its domain is in the domain set.\n\nCrucially, an exact-address filter like `billing@example.com` **does not** cover `alerts@example.com`. You'd want it to skip a real candidate only when you've explicitly created a domain filter for the whole company.\n\nThe parser handles `OR` (any case), curly-brace groups (`{a@x.com b@y.com}`), parens, quoted values, and negative terms.\n\n## Running on a schedule (headless / always-on box)\n\nThe script works fine under cron. Example: daily at 6am on Linux, pulling latest code from git first.\n\n```cron\n0 6 * * * cd /home/you/unimportant-gmail \u0026\u0026 (echo \"=== $(date) ===\" \u0026\u0026 /usr/bin/git pull --quiet \u0026\u0026 /home/you/unimportant-gmail/.venv/bin/python unimportant_gmail.py --apply --no-auth-flow) \u003e\u003e /home/you/unimportant-gmail/run.log 2\u003e\u00261\n```\n\n`--no-auth-flow` matters: if the refresh token ever becomes invalid, the default behavior is to open a browser to re-authenticate, which would just hang under cron. With the flag, the script exits with an error and you'll see it in the log.\n\nFor a headless box, do the OAuth dance once on a machine with a browser, then copy `token.json` over. The refresh token works on any machine until you explicitly revoke it.\n\nIf the script ever fails to refresh, it most likely means your OAuth consent screen is still in **Testing** mode and Google has expired the refresh token (this can happen if the app sits unused). Re-run interactively to mint a new one, or push the consent screen to **In production** to avoid expiry.\n\nTransient API failures (429 rate limits, 5xx responses) are automatically retried with exponential backoff before bubbling up.\n\n## What never gets committed\n\nThe included `.gitignore` excludes:\n\n- `credentials.json` — OAuth client secret\n- `token.json` — your access + refresh tokens\n- `senders.csv`, `diff.csv` — contain sender email addresses\n- `run.log` — same\n- `.venv/`\n\nIf you're forking this repo, keep these excluded.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbradbarrish%2Funimportant-gmail","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbradbarrish%2Funimportant-gmail","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbradbarrish%2Funimportant-gmail/lists"}