{"id":50865681,"url":"https://github.com/mrpickles007/imap-cleanup-tool","last_synced_at":"2026-06-17T06:01:23.639Z","repository":{"id":364655494,"uuid":"1268697757","full_name":"mrpickles007/imap-cleanup-tool","owner":"mrpickles007","description":"Bulk-delete or move IMAP emails by sender, domain, or nested rules - CLI + Web GUI, stdlib-only Python. Gmail support, scheduling. Windows/Linux/MacOS supported platforms.","archived":false,"fork":false,"pushed_at":"2026-06-15T00:01:03.000Z","size":1344,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-15T01:20:04.743Z","etag":null,"topics":["cli","email","email-cleanup","fastapi","gmail","imap","imap-client","inbox","inbox-cleaner","inbox-cleanup","local-webui","mailbox","spam"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/imap-cleanup-tool/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mrpickles007.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","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":"CLA.md"}},"created_at":"2026-06-13T20:44:33.000Z","updated_at":"2026-06-15T00:01:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mrpickles007/imap-cleanup-tool","commit_stats":null,"previous_names":["mrpickles007/imap-cleanup-tool"],"tags_count":106,"template":false,"template_full_name":null,"purl":"pkg:github/mrpickles007/imap-cleanup-tool","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrpickles007%2Fimap-cleanup-tool","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrpickles007%2Fimap-cleanup-tool/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrpickles007%2Fimap-cleanup-tool/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrpickles007%2Fimap-cleanup-tool/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mrpickles007","download_url":"https://codeload.github.com/mrpickles007/imap-cleanup-tool/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrpickles007%2Fimap-cleanup-tool/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34435981,"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-17T02:00:05.408Z","response_time":127,"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":["cli","email","email-cleanup","fastapi","gmail","imap","imap-client","inbox","inbox-cleaner","inbox-cleanup","local-webui","mailbox","spam"],"created_at":"2026-06-15T01:02:47.541Z","updated_at":"2026-06-17T06:01:23.631Z","avatar_url":"https://github.com/mrpickles007.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/mrpickles007/imap-cleanup-tool/main/src/imap_cleanup_tool/assets/logo.png\" alt=\"IMAP Cleanup Tool logo\" width=\"360\"\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eIMAP Cleanup Tool\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://pypi.org/project/imap-cleanup-tool/\"\u003e\u003cimg src=\"https://img.shields.io/pypi/v/imap-cleanup-tool\" alt=\"PyPI version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://pypi.org/project/imap-cleanup-tool/\"\u003e\u003cimg src=\"https://img.shields.io/pypi/pyversions/imap-cleanup-tool\" alt=\"Supported Python versions\"\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/license-AGPL--3.0--or--later-blue\" alt=\"License: AGPL-3.0-or-later\"\u003e\n\u003c/p\u003e\n\nClean your inbox **with AI, by hand, or both.** Let an **LLM** decide what is junk\nand delete it, with your choice of model:\n\n- **Free \u0026 private** - run a local model via **Ollama**, so nothing ever leaves\n  your machine; or\n- **BYOA (bring your own API key)** - point it at any cloud model (OpenAI,\n  OpenRouter, ...) using your own key.\n\nPrefer to stay in control? Write precise **sender / domain / nested-rule** filters\nyourself. Or **combine the two** - aim the AI at a single noisy domain, or let it\nsweep a whole folder.\n\nBulk-delete or move IMAP emails from the **command line** and a local **web\ninterface**. The CLI uses only the Python standard library; the web UI and the AI\nfeatures are optional extras (see [Install](#install)).\n\n\u003e 💡 **Friendly heads-up:** this tool is a *little* tricky to get the hang of -\n\u003e because it has **tons of features**. The good news: **everything is documented\n\u003e in tooltips.** Hover the little **ⓘ** icons in the web UI and you'll find a\n\u003e plain-English explanation for every single option. So take a minute with this\n\u003e README, and when in doubt, **read the tooltips** - they've got your back. 🙂\n\n- 🤖 **AI Cleanup (the headline):** a **local** heuristic scores every sender,\n  then an **LLM** decides what is junk and deletes it - with a configurable\n  threshold, a report-only mode, and per-model cost tracking. Pick your model:\n  a **free local one** via Ollama (nothing leaves your machine), **or** your own\n  cloud key (**BYOA** - OpenAI / OpenRouter / ...). Either way only sender\n  **subjects + stats** are sent, never message bodies, and it works on a filter or\n  a whole folder - just like Move. Even on a **cloud** model the cost is tiny: in\n  testing it cleaned **~13,000 emails** from a ~40k-message mailbox for about\n  **€0.03** (a local model is free). See [AI Cleanup](#ai-cleanup).\n- Prefer manual control? Match by a target file (one sender/domain per line)\n  **or** by a rule expression like `sender contains amazon.com OR (subject is\n  Invoice AND date starts 2025-01-01)`.\n- Fast **server-side search** for huge folders, or strict **local matching**.\n- **Count** how many emails a filter matches before deleting anything.\n- **Move** matched emails to another folder instead of deleting them, and\n  **create** new folders (or **labels** on Gmail) right from the app.\n- **Spam addresses**: the senders AI flags are saved per account, and you can\n  **report them as spam** to the server (train it so their *future* mail\n  auto-routes to spam).\n- **Bulk unsubscribe from newsletters**: from that same list, unsubscribe in one\n  go via each sender's `List-Unsubscribe` - **automatic** for `mailto:` (sent from\n  your SMTP profile) and one-click links, open-the-page for the rest. See\n  [Bulk unsubscribe from newsletters](#bulk-unsubscribe-from-newsletters).\n- **Email notifications**: get a mail when a cleanup/AI run finishes, with the AI\n  report attached as CSV.\n- **Gmail mode**: moves matches to Trash (the only way to truly delete on Gmail).\n- **Empty a whole folder** (e.g. Trash) without scanning.\n- **List senders** with counts and export them to CSV (with timestamp).\n- **Stop** button / cooperative cancellation for long runs.\n- **Scheduler**: save jobs (including AI jobs) and **install** them into the\n  system scheduler (Windows Task Scheduler / cron) - once, hourly, daily, weekly,\n  monthly, or every N minutes - so they run even when the app is closed, with\n  per-job logs.\n\n\u003e ⚠️ Deleting email is destructive. Always do a `--dry-run` first. Without\n\u003e `--expunge`, messages are only flagged deleted (often hidden by the client\n\u003e but recoverable until expunged).\n\n---\n\n## Table of contents\n\n- [Quick start - web interface (with AI)](#quick-start---web-interface-with-ai)\n- [Quick start - command line](#quick-start---command-line)\n- [AI Cleanup](#ai-cleanup)\n- [Install](#install)\n- [Command-line usage](#command-line-usage)\n- [Rule expressions](#rule-expressions)\n- [Target file format](#target-file-format)\n- [Web interface](#web-interface)\n- [Folders vs labels, and moving](#folders-vs-labels-and-moving)\n- [Remote / headless server (SSH port forwarding)](#remote--headless-server-ssh-port-forwarding)\n- [Scheduling](#scheduling)\n- [Email notifications](#email-notifications)\n- [Spam addresses](#spam-addresses)\n- [Bulk unsubscribe from newsletters](#bulk-unsubscribe-from-newsletters)\n- [Gmail notes](#gmail-notes)\n\n---\n\n## Quick start - web interface (with AI)\n\nThe web interface is the easiest way to use the tool - including the AI cleanup -\nand the recommended path for most users.\n\n**Prerequisite:** Python **3.10 or newer**. Check with `python --version` (or\n`python3 --version`). If it is missing, download it from\n[python.org/downloads](https://www.python.org/downloads/) - on Windows, tick\n*\"Add python.exe to PATH\"* in the installer. Linux/macOS usually ship Python, or\ninstall it with the system package manager (`sudo apt install python3 python3-pip`,\n`brew install python`, etc.).\n\n\u003e **Where do I type these?** In a **terminal**: **Command Prompt** or\n\u003e **PowerShell** on Windows (press \u003ckbd\u003eWin\u003c/kbd\u003e, type *cmd* or *PowerShell*),\n\u003e **Terminal** on macOS (Spotlight ▸ *Terminal*) or Linux. Run the commands below\n\u003e one line at a time; lines starting with `#` are comments, don't type them.\n\n**Windows** (PowerShell):\n\n```powershell\n# 1. Create and activate a virtual environment (keeps the install isolated)\npython -m venv .venv\n.venv\\Scripts\\Activate.ps1\n\n# 2. Install. [web,ai] installs the core CLI, the web UI, AND AI Cleanup\n#    (it pulls in the base package automatically - no separate step needed).\npip install \"imap-cleanup-tool[web,ai]\"\n\n# 3. Launch. Serves http://127.0.0.1:8765 and opens the default browser.\nimap-cleanup-tool-web\n```\n\n**macOS / Linux** (bash/zsh):\n\n```bash\n# 1. Create and activate a virtual environment (keeps the install isolated)\npython3 -m venv .venv\nsource .venv/bin/activate\n\n# 2. Install (core CLI + web UI + AI Cleanup in one go)\npip install \"imap-cleanup-tool[web,ai]\"\n\n# 3. Launch. Serves http://127.0.0.1:8765 and opens the default browser.\nimap-cleanup-tool-web\n```\n\n\u003e The virtual environment is recommended but optional - you can skip step 1 and\n\u003e just `pip install \"imap-cleanup-tool[web,ai]\"` globally. Either way, activate the\n\u003e same environment (`.venv\\Scripts\\Activate.ps1` / `source .venv/bin/activate`)\n\u003e in every new terminal before running `imap-cleanup-tool-web`. Don't want the AI\n\u003e features? Use `[web]` instead of `[web,ai]`.\n\u003e\n\u003e If `pip` is not found, use `python -m pip ...` (Windows) or `python3 -m pip ...`\n\u003e (macOS/Linux); on some systems the command is `pip3`. On Windows, if\n\u003e `Activate.ps1` is blocked, run\n\u003e `Set-ExecutionPolicy -Scope Process RemoteSigned` first, or use\n\u003e `.venv\\Scripts\\activate.bat` from cmd.exe.\n\nThen, in the browser:\n\n1. **Connect** - pick a provider preset (or type host/port), enter your username\n   and password (for Gmail, an **App Password** - see [Gmail notes](#gmail-notes)),\n   and click *Connect*. Optionally save it as a **connection profile** so you do\n   not retype it next time.\n2. **Pick folders** - select one or more folders to scan (each shows its message\n   count); use *Select all* / *Deselect all* as needed.\n3. 🤖 **Let AI clean it (the easy path)** - tick **AI Cleanup**, pick a model: a\n   free local **Ollama** model keeps everything on your machine, or paste your own\n   cloud API key in the **LLM** tab. Click **Generate report** to see exactly what\n   it *would* delete, then **Run**. Only subjects + stats are ever sent to the\n   model, never message bodies. See [AI Cleanup](#ai-cleanup).\n4. **Or match it yourself** - either paste a **target list** (one sender or domain\n   per line) or build a **rule** visually (field ▸ operator ▸ value, with AND/OR\n   groups). Click **Count matching emails** to see how many would be hit.\n5. **Review, then run** - *dry-run is on by default*, so the first run only\n   reports. Watch the live log; use **Stop** to cancel. When the preview looks\n   right, turn off dry-run (or pick an action - *Move to another folder*,\n   *Gmail: move to Trash*, *Expunge*) and run for real.\n6. *(Optional)* **Schedule it** - in the *Scheduling* tab, turn the same settings\n   (manual or AI) into a job and install it into the system scheduler. See\n   [Scheduling](#scheduling).\n\n\u003e On a server with no desktop, the GUI is still usable from a local browser via\n\u003e SSH port forwarding - see\n\u003e [Remote / headless server](#remote--headless-server-ssh-port-forwarding).\n\n\u003e ⚠️ Deleting email is destructive. Keep dry-run on until the count and log look\n\u003e right. Without *Expunge*, messages are only flagged deleted (often hidden by\n\u003e the client but recoverable until expunged).\n\n---\n\n## Quick start - command line\n\n```bash\n# 0. Check the installed version (update with: pip install -U imap-cleanup-tool)\nimap-cleanup-tool --version\n\n# 1. Let AI build a report of what's junk (nothing is deleted), saved to CSV.\n#    Heuristic-only here (no model) - works offline; needs the [ai] extra.\nimap-cleanup-tool --host imap.gmail.com --user you@gmail.com \\\n    --ai-cleanup --ai-report-only --ai-report-csv report.csv\n\n# 2. Run AI Cleanup for real with a configured model (omit --dry-run to delete).\n#    Use a local Ollama model to keep everything on your machine.\nimap-cleanup-tool --host imap.gmail.com --user you@gmail.com \\\n    --ai-cleanup --ai-model my-model --dry-run\n\n# 3. Or do it by hand: see folders, preview a target/rule cleanup, then run.\nimap-cleanup-tool --host imap.gmail.com --user you@gmail.com --list-folders\nimap-cleanup-tool --host imap.gmail.com --user you@gmail.com \\\n    --targets targets.txt --dry-run\nimap-cleanup-tool --host imap.gmail.com --user you@gmail.com \\\n    --targets targets.txt --gmail-trash\n```\n\n\u003e AI Cleanup needs the **`[ai]`** extra: `pip install \"imap-cleanup-tool[ai]\"`.\n\u003e Configure a model in the web **LLM** tab (or point at a local Ollama model).\n\u003e See [AI Cleanup](#ai-cleanup) for the full set of `--ai-*` flags.\n\nCredentials are read from flags, then environment variables\n(`IMAP_HOST`, `IMAP_USER`, `IMAP_PASSWORD`, `IMAP_PORT`), then an interactive\nprompt. Prefer the prompt or env vars over `--password` so the secret does not\nland in your shell history.\n\n---\n\n## AI Cleanup\n\n*Optional - install the AI extra:* `pip install \"imap-cleanup-tool[ai]\"`.\n\n\u003e **Local-first, and BYOA (Bring Your Own API key).** AI Cleanup runs great on a\n\u003e **free local model** (Ollama) so nothing ever leaves your machine - or you can\n\u003e **bring your own API key** for any cloud model (OpenAI, OpenRouter, ...). Your\n\u003e key, your model, your choice. Either way, only sender **subjects + stats** are\n\u003e sent to the model - **never the message body**.\n\nAI Cleanup hands \"which of these do I actually want?\" to a model, safely - and\n**efficiently**. The key design choice: it works on **aggregated per-sender\nstatistics**, never on your individual emails. It never feeds a whole mailbox to\nan LLM (that would be slow and make the token count explode); a **local heuristic\ndoes the bulk of the work**, and only a small shortlist of borderline **senders**\n(with a few sample subjects each) ever reaches the model.\n\n1. **Heuristic pre-filter (local), per sender.** It groups your mail **by sender**\n   (not per individual email) and gives each sender a 0-10 **spam score** from\n   signals read on your machine: `List-Unsubscribe`, the share of **unread**\n   messages, send **frequency**, `Precedence: bulk`, and sender patterns\n   (`noreply@`, `newsletter@`...). Weights are calibrated and **tunable**. This\n   local engine does most of the filtering, fast and for free.\n2. **LLM verdict.** Only senders at or above your **threshold** (default 6) are\n   sent to the model, with a few sample **subjects** each (never the body); it\n   replies in strict JSON which to delete. The prompt has an explicit\n   **safeguard**: it must KEEP anything that looks like online orders/receipts,\n   appointments/bookings, medical/health, travel, banking/tax, security/2FA, or\n   personal mail - only obvious bulk (newsletters, promotions, notifications) is\n   marked deletable. The reply is **validated with pydantic**, and the model is\n   **retried up to 3 times** before giving up.\n3. **Verdict to action** - see the two buttons below.\n\n\u003e 💸 **Real-world cost \u0026 speed.** In our testing, running AI Cleanup with\n\u003e **`gpt-4o-mini`** over a **~40,000-message** Gmail mailbox cost about **€0.03**\n\u003e (a few cents) and cleaned roughly **13,000 emails in ~5 minutes**. Cost scales\n\u003e with how many senders cross the threshold (only those go to the LLM, a few\n\u003e subjects each), so your mileage varies - and a **local Ollama model costs\n\u003e nothing at all**.\n\u003e\n\u003e 📉 **The more you run, the less it costs.** Every run saves the flagged senders\n\u003e to your [Spam addresses](#spam-addresses) list, and **Check spam addresses**\n\u003e (on by default) skips those known senders from the LLM on later runs - so each\n\u003e cleanup sends **fewer addresses to the model** than the last. It gets cheaper\n\u003e the more you use it.\n\n### Generate report vs Run\n\n- **Generate report** - builds the report and **changes nothing**. By default it\n  also asks the LLM for a verdict on each flagged sender (so the report shows what\n  *would* be deleted); download it as **CSV** (Excel-friendly), and the log shows\n  how many emails are potentially deletable. CLI: `--ai-cleanup --ai-report-only`.\n- **Skip LLM (heuristic only)** - a small checkbox next to the buttons. When\n  ticked, **Generate report uses only the local heuristic score** - **no LLM call**,\n  so it is free, much faster, and nothing leaves your machine; the report simply\n  has no per-sender AI verdicts. CLI: `--ai-report-only` *without* `--ai-model`.\n- **Run** - builds the report **and deletes** the senders the LLM confirms\n  (dry-run simulates). **Run always calls the chosen LLM model, even if \"Skip LLM\"\n  is ticked** - deleting is driven by the LLM verdict, so a model is required for\n  Run. \"Skip LLM\" only affects *Generate report*, never *Run*. CLI: `--ai-cleanup\n  --ai-model NAME` (omit `--dry-run` to actually delete).\n\nEvery report (from Generate report or Run) is **auto-saved to disk as a\ntimestamped CSV** in your config directory (`ai_reports/`), so reports stay\navailable after other runs or a restart. Reports are saved **per account** (the\naccount is in the file name), and the **dropdown** next to the buttons lists only\nthe **connected mailbox's** reports, newest-first - it refreshes automatically\nwhen you connect or switch account. Pick one and click **Download CSV**, or\n**Delete** to remove that saved report (the CSV file only - it does not touch any\nemail). If **email notifications for interactive runs** are enabled\n(see [Email notifications](#email-notifications)), *Generate report* also **emails\nyou the CSV** as an attachment (named like the saved file).\n\n**Flag senders as spam (on Run).** Optionally, when **Run** deletes a confirmed\nsender, first move **one** of their messages to the **Junk/Spam** folder - the\nstandard \"report spam\" signal that trains the server to route that sender's\n**future** mail to spam - then delete the rest. This also works in scheduled AI\njobs (a checkbox) and on the CLI (`--ai-flag-spam`). It needs a Junk/Spam folder\non the server.\n\n**Check spam addresses (saves LLM tokens, on by default).** Flagged senders that\nare **already in this account's [Spam addresses](#spam-addresses) list** (from\nearlier reports/runs) are accepted as spam **without asking the model again**, so\nfewer addresses are sent to the LLM - real token savings on repeat runs. They are\ntreated as confirmed for deletion with a synthetic verdict (\"already in saved spam\nlist\"). It is a checkbox in the AI panel and in scheduled AI jobs (CLI:\n`--ai-no-check-spam` to turn it off). **Edge case:** an important email from a\nsender already on the list would be treated as spam - this is rare, and avoided by\nrunning with the option **off** (then every flagged sender is re-evaluated by the\nLLM).\n\nAI Cleanup deletes the **same way as a normal run**: on a regular server the\nmessages are flagged `\\Deleted` and, if you tick **Expunge**, immediately removed\nfor good (otherwise they linger until an expunge). On **Gmail** they are moved to\nthe **Trash** and are not permanently gone until the Trash is emptied (the UI\nreminds you and offers to set that up - see [Gmail notes](#gmail-notes)). On the\nCLI add `--expunge` for permanent removal.\n\nYour own mailbox address is **pre-filled in the Exclude box when you connect**, so\nself-sent mail is skipped by default. **Remove that line** if you want your own\naddress included too, and add any other senders to skip (one per line). On the CLI\nthe same default applies - pass `--ai-include-self` to include your own address,\nor `--ai-exclude ADDR` to skip more.\n\nLike **Move**, AI Cleanup honors the active **filter** (target list or rule) when\none is set, or scans the **whole folder** when none is - so you can point it at a\nsingle noisy domain or let it sweep everything.\n\n**Models** are configured in the **LLM** tab (powered by litellm). On first run\nthe tool seeds two ready-to-use defaults you can edit or delete: **`gpt-4o-mini`**\n(cloud, no key stored - set `OPENAI_API_KEY` or paste a key) and\n**`ollama-llama3`** (free, local via Ollama). More options:\n\n- **Local \u0026 private (recommended):** an Ollama model (e.g. `ollama/llama3`) keeps\n  everything on your machine. ⚠️ A **remote** model (OpenAI, OpenRouter, ...)\n  sends the sample subjects to that provider - the app warns you, and only ever\n  sends subjects + stats, never message bodies.\n- **Edit** a saved model from the list (the **edit** button loads it into the\n  form). The key is never shown - leave the key field blank to keep the current\n  one, or type a new one to replace it.\n- API keys live in a local SQLite DB, optionally **encrypted** (encrypted = not\n  usable in scheduling, like connection profiles). Keys are never committed.\n- **Prefer an environment variable?** Leave the model's API-key field **blank**\n  and export the provider's standard variable instead - e.g.\n  `OPENAI_API_KEY` (OpenAI), `OPENROUTER_API_KEY` (OpenRouter). litellm picks it\n  up automatically, so the key never touches disk. (PowerShell:\n  `$env:OPENAI_API_KEY = \"sk-...\"`; bash: `export OPENAI_API_KEY=sk-...`.)\n- Optional **cost tracking**: set the price per million tokens and get a\n  per-model cost log.\n\nAI Cleanup can also be **scheduled** (Scheduling tab -\u003e \"AI Cleanup job\") with a\nnon-encrypted model; the scheduled CLI runs `--ai-cleanup --ai-model NAME`.\n\nEverything the web panel offers is available on the CLI too - threshold, sample\nsize, exclusions, heuristic weights, report-only, and CSV export:\n\n```bash\npip install \"imap-cleanup-tool[ai]\"\n# Configure a model + API key in the LLM tab (or a local Ollama model), then:\nimap-cleanup-tool --host HOST --user USER \\\n    --ai-cleanup --ai-model my-model --dry-run\n\n# Heuristic report only (no LLM, nothing deleted), saved to CSV:\nimap-cleanup-tool --host HOST --user USER \\\n    --ai-cleanup --ai-report-only --ai-report-csv report.csv\n\n# Tune threshold/weights and add exclusions, with an LLM, report only:\nimap-cleanup-tool --host HOST --user USER \\\n    --ai-cleanup --ai-model my-model --ai-report-only \\\n    --ai-threshold 7 --ai-weight unread_ratio=4 --ai-weight bulk=2 \\\n    --ai-exclude boss@work.com --ai-report-csv report.csv\n```\n\n\u003e If you run an `--ai-cleanup` command without the `[ai]` extra installed, the CLI\n\u003e stops with a clear message telling you to `pip install \"imap-cleanup-tool[ai]\"`.\n\n### Local header cache (faster repeat reports)\n\nFetching message headers is the slow part of building a report - on a slow IMAP\nserver it can take **several seconds per 50 messages** (a Gmail-class server does\nthe same in 1-2s). Without a cache, every report's speed depends **entirely on\nyour IMAP server**, so on a slow provider each report is slow again.\n\nThat is why **Enable local cache** (in the connection card) is **on by default**.\nYou can untick it; the setting is **saved with the connection profile**, and on\nthe CLI it is opt-in via `--local-cache` (or carried by a saved profile). With it\non, the tool caches the **immutable** header fields on your machine, keyed by\nmessage **UID**: `From`, `Date`, `Subject`, the `List-Unsubscribe` /\n`List-Unsubscribe-Post` info (so one-click unsubscribe data is cached too) and the\n`Precedence: bulk` marker. **No message bodies** are ever stored, and the volatile\n`\\Seen` (read/unread) flag is **not** cached - it is always re-read fresh so unread\ncounts stay accurate. The next time, only the **new** messages are fetched, so\nrepeat reports are near-instant. The cache is **per account**.\n\nThe cache applies to **every operation that downloads headers**: AI reports/runs,\n**List senders**, and matching with **`--scan-mode full`** (interactive *and*\nscheduled jobs - they share the same cache). It does **not** apply to the default\nserver-side **`search`** mode (move / delete / count / list-senders in `search`\nmode), because there the **server** does the filtering and no headers are\ndownloaded at all - so there is nothing to cache there.\n\n\u003e **First run on a new mailbox is the slow one.** The *first* report on a mailbox\n\u003e still fetches every header (it can take a few minutes on a big, slow mailbox) -\n\u003e that's it filling the cache. Every report after that is fast.\n\n**The flag just controls whether this run reads/writes the cache.** With it\n**off**, headers are **fetched fresh from the server every time and not stored**\n(the slow path), and an existing cache is simply **left untouched and ignored** -\nnothing is deleted. Re-tick **Enable local cache** any time and it **self-heals**: it reuses\nwhat's already cached and fetches only the messages that arrived in the meantime\n(headers never change, so old entries stay valid; entries for deleted messages are\njust never looked up). The CLI behaves the same (an existing cache is left intact\nand ignored, with a note suggesting `--local-cache`).\n\nTo wipe the cache on purpose, use the **Clear cache** button - it appears under\nthe checkbox when the connected account has cached headers and shows **how many\nare stored**.\n\nIt stays correct: headers never change, and the volatile `\\Seen` flag is always\nre-read fresh (a cheap fetch) so unread counts stay accurate. The cache is pinned\nto each folder's **UIDVALIDITY** (the IMAP value that changes only if the server\nrenumbers messages - folder recreated, mailbox migrated, ...); if it changes, the\nstale rows are dropped and headers are re-fetched. Stored locally in a small\nSQLite file (`header_cache.sqlite`) in your config directory.\n\n---\n\n## Install\n\nRequires **Python 3.10 or newer** (`python --version`). See\n[python.org/downloads](https://www.python.org/downloads/) if you need it.\n\n### From PyPI (recommended)\n\nA virtual environment keeps the install isolated (optional but recommended):\n\n```bash\npython -m venv .venv\n# Windows:      .venv\\Scripts\\activate\n# macOS/Linux:  source .venv/bin/activate\n```\n\nThen install. The base package is the CLI only; the `[web]` extra adds the web UI\nand `[ai]` adds AI Cleanup (each pulls in the base package automatically):\n\n```bash\npip install imap-cleanup-tool             # core CLI only (no AI, no web UI)\npip install \"imap-cleanup-tool[web]\"      # core CLI + web UI (imap-cleanup-tool-web)\npip install \"imap-cleanup-tool[ai]\"       # core CLI + AI Cleanup (litellm)\npip install \"imap-cleanup-tool[web,ai]\"   # everything (recommended)\n```\n\nYou do not need to install the base separately before an extra - it is included.\nThe CLI stays dependency-free; the `[web]` extra pulls in FastAPI/uvicorn (and\ncryptography for encrypted profiles), and the **`[ai]` extra** pulls in\n**`litellm`** for [AI Cleanup](#ai-cleanup) (cloud models or a local Ollama one).\nWant the AI features but not the web UI? `pip install \"imap-cleanup-tool[ai]\"`.\n\n### From source\n\n```bash\ngit clone https://github.com/mrpickles007/imap-cleanup-tool.git\ncd imap-cleanup-tool\n\npython -m venv .venv\nsource .venv/bin/activate        # Windows: .venv\\Scripts\\activate\n\npip install -e \".[dev,web,ai]\"   # editable install + dev tools + web UI + AI\n```\n\n### Running the tests\n\nThe test suite uses only the standard library (`unittest`) - nothing extra to\ninstall:\n\n```bash\npython -m unittest discover -s tests -v\n```\n\n---\n\n## Command-line usage\n\n| Option | Meaning |\n| --- | --- |\n| `-V`, `--version` | Print the installed version and exit. |\n| `--host`, `--port`, `--user`, `--password` | Connection (port default 993). |\n| `--timeout N` | Socket timeout in seconds (default 120). |\n| `--folder NAME` | Folder to scan; repeat for several. Default `INBOX`. |\n| `--targets FILE` | Match by a target list file. |\n| `--rule \"EXPR\"` | Match by a rule expression (see below). |\n| `--scan-mode search\\|full` | Server-side search (fast) or local match (strict). |\n| `--include-subdomains` | In `full` mode, also match subdomains. |\n| `--batch-size N` | Messages per IMAP request (default 500). |\n| `--local-cache` | Cache message headers locally so repeat AI reports are faster (also enabled by a profile's setting). |\n| `--list-folders` | Print folders and exit. |\n| `--list-senders` | Print unique senders with counts and exit. |\n| `--save-senders CSV` | With `--list-senders`, append to a CSV. |\n| `--empty-folder` | Delete ALL messages in the folder(s); no filtering. |\n| `--gmail-trash` | Move matches to Gmail Trash via labels. |\n| `--move` | Move matches to `--dest-folder` (or **all** messages if no `--targets`/`--rule`). |\n| `--dest-folder NAME` | Destination folder/label for `--move`. |\n| `--create-folder NAME` | Create a folder (a label on Gmail) on the server, then exit. |\n| `--delete-folder NAME` | Delete a non-system folder/label on the server, then exit. |\n| `--ai-cleanup` | AI cleanup: heuristic score -\u003e LLM verdict -\u003e delete confirmed senders (needs `[ai]`). |\n| `--ai-model NAME` | Saved (non-encrypted) LLM model config to use for `--ai-cleanup`. |\n| `--ai-threshold N` | Heuristic spam-score threshold 0-10 (default 6). |\n| `--ai-sample N` | Sample emails per flagged sender (default 5). |\n| `--ai-exclude ADDR` | Extra sender to exclude from the report (repeatable). Your own address is excluded by default. |\n| `--ai-include-self` | Include your own mailbox address in the report (by default it is excluded). |\n| `--ai-weight KEY=VALUE` | Override a heuristic weight (repeatable): `list_unsubscribe`, `unread_ratio`, `bulk`, `sender_pattern`, `frequency`. |\n| `--ai-report-only` | Build the report (and LLM verdicts if `--ai-model` is given) but delete nothing; a model is optional. |\n| `--ai-report-csv PATH` | Write the report as CSV (Excel-friendly) to `PATH`. |\n| `--ai-flag-spam` | On delete, first move one message per confirmed sender to Junk/Spam (trains the server), then delete the rest. Needs a Junk/Spam folder. |\n| `--ai-no-check-spam` | Re-evaluate every flagged sender with the LLM. By default, senders already in the saved Spam list are accepted as spam without asking the model (saves tokens). |\n| `--dry-run` | Report only; make no changes. |\n| `--expunge` | Permanently remove after flagging. |\n| `--yes` | Skip the confirmation prompt (for scripts/cron). |\n| `--verbose`, `-v` | Debug logging with per-batch progress. |\n| `--run-job NAME` | Run a saved scheduled job by name (used by the OS scheduler). |\n| `--profile NAME` | Load host/user/password from a saved, non-encrypted profile. |\n\nExamples:\n\n```bash\n# Save a sender report (timestamp, account, folder, sender, count)\nimap-cleanup-tool --host HOST --user USER --list-senders --save-senders senders.csv\n\n# Empty the Trash, fast, no scan\nimap-cleanup-tool --host HOST --user USER --folder Trash --empty-folder --dry-run\nimap-cleanup-tool --host HOST --user USER --folder Trash --empty-folder\n\n# Strict local matching including subdomains\nimap-cleanup-tool --host HOST --user USER --targets targets.txt \\\n    --scan-mode full --include-subdomains --dry-run\n\n# Create a folder/label, then MOVE matched mail into it (instead of deleting)\nimap-cleanup-tool --host HOST --user USER --create-folder \"Archive/2025\"\nimap-cleanup-tool --host HOST --user USER --targets targets.txt \\\n    --move --dest-folder \"Archive/2025\" --dry-run\n```\n\n---\n\n## Rule expressions\n\nIn the **web UI** you build rules visually with the query builder (no typing).\nThe text grammar below is what the **CLI** `--rule` flag accepts and what\nscheduled jobs store - the visual builder produces exactly these expressions\nunder the hood.\n\nRules are an alternative to target files, evaluated server-side via IMAP\n`SEARCH`. Fields and operators:\n\n| Field | Operators | Maps to |\n| --- | --- | --- |\n| `sender` | `is`, `contains` | `FROM` |\n| `subject` | `is`, `contains` | `SUBJECT` |\n| `date` | `is`, `starts`, `ends` | `ON`, `SINCE`, `BEFORE` |\n\nCombine conditions with `AND` / `OR`, and group with parentheses for nesting.\nDates accept `YYYY-MM-DD`. Quote values with spaces.\n\n```bash\nimap-cleanup-tool --host HOST --user USER --dry-run \\\n    --rule 'sender contains amazon.com OR (subject is \"Black Friday\" AND date starts 2025-11-01)'\n```\n\n\u003e `is` and `contains` both map to IMAP substring matching on the header; IMAP\n\u003e has no exact-header match, so treat `contains` as the reliable operator and\n\u003e use the target-file `--scan-mode full` path when you need strict exactness.\n\n---\n\n## Target file format\n\nOne entry per line; `#` starts a comment.\n\n```text\nspam@example.com        # exact sender address\n*@newsletter.com        # that domain EXACTLY - never subdomains\nannoying.com            # that domain, plus subdomains if --include-subdomains\nmail.annoying.com       # that specific (sub)domain\n```\n\nThe `*@domain` form always matches the domain exactly; the bare `domain` form\nalso matches subdomains when `--include-subdomains` is given. This distinction\napplies to local `--scan-mode full`; server-side `search` is a substring match\neither way.\n\nSo in `full` mode `*@paypal.com` is the same as `paypal.com` *without*\n`--include-subdomains`. The useful part is mixing them: with\n`--include-subdomains` **on**, `*@paypal.com` stays exact while bare domains\nexpand to their subdomains - per-entry control in a single list. Example:\n\n```text\n*@paypal.com      # exact, even with --include-subdomains\nnewsletter.com    # this one DOES include its subdomains\n```\n\n---\n\n## Web interface\n\nA local web UI (FastAPI) is the tool's graphical interface. Install the extra\nand run:\n\n```bash\npip install \"imap-cleanup-tool[web,ai]\"\nimap-cleanup-tool-web        # serves http://127.0.0.1:8765 and opens your browser\n```\n\nOptions: `--host`, `--port`, `--no-browser`. It runs only on your machine\n(`127.0.0.1`) by default. The IMAP connection lives on the local server and is\nreused across actions, surviving a page refresh; it is dropped automatically\nafter a period of inactivity. Your password is never stored.\n\nHighlights:\n\n- 🤖 **AI Cleanup** with a model dropdown (local Ollama or your own cloud key),\n  a threshold slider, Generate report / Run, and per-model cost tracking - see\n  [AI Cleanup](#ai-cleanup). The **LLM** tab has a **model picker** (presets per\n  provider, an **✎ edit** toggle to type any custom litellm id, and the option to\n  save or remove your own presets); the **API key is optional** - you can set the\n  provider's env var (`OPENAI_API_KEY`, …) instead. When the `[ai]` extra is\n  missing, the AI option is disabled with a banner explaining how to install it.\n- Many provider presets, connect-and-load-folders (with per-folder message\n  counts), multi-folder selection, Select all / Deselect all.\n- **Connection profiles**: save host / user / password to a local SQLite DB -\n  optionally **encrypted** with a password, with a per-profile **Enable local\n  cache** toggle (see [AI Cleanup](#ai-cleanup)) - and pick one from a dropdown.\n- Match by a **target list** (paste or load from a file, with inline format\n  help) or a **visual nested query builder** (field ▸ operator ▸ value, AND/OR\n  groups).\n- **Count matching emails** before deleting; **dry-run** is on by default.\n- **Move** matches to another folder instead of deleting (pick the destination\n  from a dropdown of your folders, or create a new one inline) - or **every**\n  message if you leave the filter empty; plus\n  **create** and **delete** folders/labels on the server (system folders are\n  protected). The folder box distinguishes *Add to scan* (just lists a folder to\n  scan, creates nothing) from *Create on server* (really creates a folder, a\n  **label** on Gmail) - see [Folders vs labels](#folders-vs-labels-and-moving).\n- Context-aware options with tooltips (e.g. *Include subdomains* only in\n  `\"full\"` scan mode; *Gmail: move to Trash* only for Gmail).\n- Background runs with a **Stop** button and a persistent, live log panel.\n- **List senders** with counts (export to CSV), a **Spam addresses** tab, an\n  **Email notifications** tab, and a **Scheduling** tab to create jobs and install\n  them into the OS scheduler.\n- A **light / dark theme** toggle (top bar on desktop, in the menu on mobile); your\n  choice is remembered. The brand colors stay the same in both.\n\n---\n\n## Folders vs labels, and moving\n\nOver IMAP a \"folder\" and a Gmail \"label\" are the same thing, so creating one\nworks everywhere: on a normal mailbox you get a folder, on Gmail you get a label.\n\nTwo different actions in the app are easy to confuse, so they are kept distinct:\n\n- **Add to scan** (the folder box) only adds a name to the list of folders you\n  will scan. It does **not** create anything on the server - use it to scan a\n  folder that was not auto-listed.\n- **Create on server** actually creates a new folder/label on your mailbox (via\n  IMAP `CREATE`). Use it to make a destination before moving.\n- **Delete on server** (the 🗑 on a folder row, or `--delete-folder`) removes a\n  folder/label from your mailbox. Only folders **you** created can be deleted;\n  **system folders are protected** (see below).\n\n**How \"system folders\" are detected.** The app does **not** use a hardcoded list\nof names (that would break on Gmail and on non-English mailboxes). Instead it\nreads each folder's IMAP attributes from the `LIST` response and protects any\nfolder the server marks as **special-use** (RFC 6154): `\\All`, `\\Archive`,\n`\\Drafts`, `\\Flagged`, `\\Junk`, `\\Sent`, `\\Trash`, `\\Important`, or `\\Noselect`\n(a non-selectable container like `[Gmail]`) - plus **INBOX**, always. So\n`[Gmail]/Trash`, the localized `[Gmail]/Cestino`, *Sent Mail*, Drafts, etc. are\nrecognized automatically by their flags, not their names. (Backstop: even if a\nserver failed to flag a special folder, its own `DELETE` would still refuse it.)\nIn the web UI those protected folders simply don't show the 🗑 button.\n\nWhen you choose **Move**, the destination is a **dropdown of your existing\nfolders** - pick one, or choose *➕ Create new…* to make a new folder/label on the\nspot (no typing the name unless you are creating one).\n\n**Moving** copies the matched messages into the destination and removes them from\nthe source. The tool uses the server's `MOVE` command when available, otherwise\n`COPY` + delete + expunge. On **Gmail** a move *relabels* the messages (removes\nthe source label, adds the destination one); the message itself still lives in\n*All Mail*. Move is mutually exclusive with delete / Gmail-trash / expunge; only\n*Empty folder* overrides everything.\n\nYou **cannot move a folder into itself**: the web destination dropdown lists only\nfolders **not** selected as the source, and the core skips any move where source\n== destination (with a warning) - so the CLI and scheduled jobs are protected\ntoo. (IMAP does not guard this reliably: the `COPY` + delete fallback could even\nduplicate the messages.)\n\n**Move everything.** If you enable Move **without** a target list or rule, it\nmoves **every** message in the selected folders into the destination (handy to\nclear out or reorganize a whole folder). From the CLI, just omit `--targets` and\n`--rule`:\n\n```bash\nimap-cleanup-tool --host HOST --user USER --create-folder \"Receipts\"\n\n# Move only matches\nimap-cleanup-tool --host HOST --user USER --targets bills.txt \\\n    --move --dest-folder \"Receipts\" --dry-run\n\n# Move EVERYTHING from INBOX into Archive (no filter)\nimap-cleanup-tool --host HOST --user USER --folder INBOX \\\n    --move --dest-folder \"Archive\" --dry-run\n```\n\nMove jobs can be **scheduled** like any other job (the Scheduling tab carries the\nsame Move setting and destination into the saved job).\n\n---\n\n## Remote / headless server (SSH port forwarding)\n\nThe tool can be installed on a **remote, desktop-less server** (e.g. a VPS or a\nhome server reached over SSH) and still be driven through the **web GUI in a\nlocal browser**. The web server binds to the server's loopback\n(`127.0.0.1:8765`) and is **not** exposed to the network; the browser reaches it\nthrough an encrypted **SSH tunnel** that maps a local port to that loopback port.\nThis is the same \"local port forwarding\" mechanism used by the *VS Code Remote*\nextension and SSH clients such as *Bitvise* or *PuTTY*.\n\n**On the server** (in the SSH session), start the web server without trying to\nopen a browser it does not have:\n\n```bash\npip install \"imap-cleanup-tool[web]\"\nimap-cleanup-tool-web --no-browser          # listens on 127.0.0.1:8765\n```\n\n**On the local machine**, open an SSH tunnel that forwards a local port to the\nserver's `127.0.0.1:8765`:\n\n```bash\n# Forward local 8765  -\u003e  server's localhost:8765\nssh -N -L 8765:localhost:8765 user@your-server\n```\n\nThen open **http://localhost:8765** in your local browser. Traffic travels inside\nthe SSH connection; nothing is published on the server's public interface.\n\n- **VS Code Remote-SSH**: open the folder on the server, run `imap-cleanup-tool-web\n  --no-browser` in its terminal - VS Code auto-forwards the port and offers to\n  open it locally. (Add it manually in the *Ports* panel if needed.)\n- **Bitvise / PuTTY**: add a *Local* (C2S) forwarding rule - listen interface\n  `127.0.0.1`, listen port `8765`, destination host `localhost`, destination\n  port `8765` - then browse to `http://localhost:8765`.\n- Pick a different **local** port if 8765 is busy, e.g. `-L 9000:localhost:8765`\n  → open `http://localhost:9000`. To run the server on another port, use\n  `imap-cleanup-tool-web --no-browser --port 8800` and forward to that.\n\n\u003e **Keep it on loopback.** Prefer the SSH tunnel over `--host 0.0.0.0` (which\n\u003e would expose the unauthenticated UI to the whole network). The tunnel gives you\n\u003e SSH's authentication and encryption for free.\n\n\u003e **Keep it running after logout.** A plain SSH session stops the server when you\n\u003e disconnect. To leave it running, start it under `tmux`/`screen`, with\n\u003e `nohup imap-cleanup-tool-web --no-browser \u0026`, or as a `systemd` service. For\n\u003e *unattended* recurring cleanups you usually want a **scheduled job** instead of\n\u003e a long-lived server - see [Scheduling](#scheduling).\n\n---\n\n## Scheduling\n\nJobs are stored as JSON in your user config directory\n(`%APPDATA%\\imap-cleanup-tool` on Windows, `~/.config/imap-cleanup-tool` elsewhere).\nScheduling is handled entirely by the **operating system scheduler** - there is\nno background process to keep running.\n\nClick *Install to system scheduler* to register a job directly (a `schtasks`\ntask on Windows, a `crontab` line on Linux/macOS) so it runs even when the app\nis closed. *Export command* shows the equivalent line.\n\n**Frequency** - pick one in the *Scheduling* tab; the form shows only the inputs\nthat apply:\n\n| Frequency | Inputs | Windows | Linux/macOS |\n| --- | --- | --- | --- |\n| Run once | date + time | `schtasks /SC ONCE` | `at` (must be installed) |\n| Every N minutes | minutes | `/SC MINUTE /MO N` | `*/N * * * *` |\n| Hourly | minute of hour | `/SC HOURLY` | `M * * * *` |\n| Daily | time | `/SC DAILY` | `MM HH * * *` |\n| Weekly | weekday + time | `/SC WEEKLY /D` | `MM HH * * \u003cdow\u003e` |\n| Monthly | day 1-28 + time | `/SC MONTHLY /D` | `MM HH \u003cdom\u003e * *` |\n\nThe time/date pickers use your system locale; the one-time date is rendered in\nthe system's short-date format for `schtasks`. One-time jobs on Linux/macOS use\n`at`: the tool records the `at` job number on install, so they show as\n*installed* (via `atq`) and can be uninstalled from the panel (via `atrm`),\njust like recurring cron jobs. A one-time job that has already fired drops back\nto *saved* (it is no longer queued).\n\n\u003e **Linux/macOS one-time jobs need `at`.** The `at`/`atq`/`atrm` commands must\n\u003e be installed **and** the `atd` daemon must be running, otherwise the job will\n\u003e not fire. On many distributions `at` is not installed by default:\n\u003e `sudo apt install at` (Debian/Ubuntu) or `sudo dnf install at` (Fedora), then\n\u003e enable the daemon with `sudo systemctl enable --now atd`. (macOS ships `at`,\n\u003e but `atrun` is disabled by default - enable it with\n\u003e `sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.atrun.plist`.)\n\u003e Recurring jobs use cron instead and have no such requirement.\n\nEach job connects with a saved **connection profile** (chosen in the Scheduling\ntab), so different jobs can target different accounts. The scheduled task runs\nthe job by name (`imap-cleanup-tool --run-job NAME`) via the current interpreter\n(so it works inside your virtualenv without relying on `PATH`); at run time the\nCLI loads host / user / password from the profile's local SQLite DB. Only\n**non-encrypted** profiles can be scheduled - a cron has no way to type the\npassword to decrypt an encrypted one.\n\n**AI Cleanup jobs** - tick *AI Cleanup* in the Scheduling tab to schedule the AI\nflow. Two extra options:\n\n- **Report only** - the job builds the report and **deletes nothing**. The report\n  is saved on disk (and listed in the *Download* dropdown), and if **email\n  notifications for jobs** are enabled it is sent as a **CSV attachment**.\n- **Skip LLM (heuristic only)** - build the report from the local heuristic only,\n  with **no LLM call** (no API cost). It requires *Report only* (the heuristic\n  alone can't decide what to delete). Both options can be combined.\n\nA non-report-only AI job needs a **non-encrypted** LLM model (to run unattended).\nOn the CLI these map to `--ai-cleanup --ai-report-only` and omitting `--ai-model`.\n\n**Logs** - every scheduled run appends to a rolling log file under\n`\u003cconfig dir\u003e/logs/\u003cjob\u003e.log`. In the *Scheduling* tab, click **logs** on any\nsaved job to view (or download) its run history.\n\n---\n\n## Email notifications\n\nGet an email when a cleanup finishes. Configure it in the **Notifications** tab:\n\n- **SMTP profiles** - save one or more outgoing-mail servers (host, port,\n  security, username, password, From address). Works with any provider - Gmail,\n  **Amazon SES**, Outlook/Microsoft 365, SendGrid, Mailgun, Postmark, Brevo, etc.\n  - the provider dropdown prefills host/port/security and shows **per-provider\n  tooltips** on the username/password fields. The password is stored locally\n  (SQLite) and can be **encrypted** with a passphrase, exactly like connection\n  profiles - the passphrase needs a **confirmation**, a show/hide toggle, and\n  meets **strength criteria** before you can save (an encrypted profile can't run\n  in scheduled jobs). Each profile has a **test connection** button.\n- **One active profile** + a recipient address. Toggle notifications for\n  **scheduled jobs** (default) and/or **interactive runs**. A **Send test email**\n  button confirms it all works. With **interactive runs** on, you get an email\n  when a cleanup or AI run finishes - and *Generate report* emails you the **AI\n  report CSV** as an attachment.\n- For a **Gmail** account, the email reminds you that the messages were moved to\n  the Trash and must be emptied to delete them for good (e.g. schedule an\n  *Empty folder* job on `[Gmail]/Trash`).\n\nSending uses the Python standard library (`smtplib`), so notifications also fire\nfor scheduled CLI jobs. Scheduled jobs require a **non-encrypted** active profile\n(no one is there to type the passphrase).\n\n---\n\n## Spam addresses\n\nEvery **AI Cleanup** report or run records the flagged senders (the potential\nspam) into a per-account list, shown in the **Spam addresses** tab. Each\nconnected mailbox has its own list.\n\nFor every address you see the data the tool computed - the 0-10 **heuristic spam\nscore**, message count, unread ratio, weekly frequency, the signals\n(`List-Unsubscribe`, bulk, sender pattern), and the **LLM verdict** (keep/delete +\nreason + confidence) when a model was used.\n\n- **Browse** with search and **pagination** (rows per page configurable). Click\n  any **column header to sort** the whole list (score, msgs, unread, msgs/week,\n  signals, verdict); pagination is kept.\n- **Select** rows (or *select all* across pages) for **bulk** actions.\n- **Remove from list** - drops them from this list only (does not touch the\n  mailbox).\n- **Flag senders as spam** - for each selected sender it scans the **folders\n  selected in the Cleanup tab** (just like a run; the popup shows which), finds\n  their mail and reports them to the server's spam filter (so **future** mail is\n  auto-routed to spam). A popup lets you choose: **move one message to Junk/Spam\n  and delete the rest** (same as Run), or **move all to Spam** and delete nothing.\n  Moving mail to the Junk/Spam folder (found via its special-use flag, so it works\n  with localized names) is the standard \"report spam\" training signal. It reports\n  any addresses that had no mail (e.g. already deleted). You can also flag senders\n  **during cleanup** (the *Flag senders as spam* option above).\n- **Add a sender manually** - type an address (with an optional 0-10 score) and\n  **Add** to put it on this account's list yourself, alongside the AI-flagged ones.\n- **Unsubscribe (newsletters)** - select senders and click **Unsubscribe** to use\n  their `List-Unsubscribe` header. See [how it works](#bulk-unsubscribe-from-newsletters) below.\n\n### Load saved spam into a Target list\n\nThe spam list doubles as a reusable **blocklist**. In the **Cleanup** tab, when\nsaved spam addresses exist for the connected account, a **Load saved Spam\naddresses** box appears under the Target list. Pick a **score** condition\n(`is` / `\u003c=` / `\u003e=` / `\u003c` / `\u003e`) and a threshold (default 6, step 0.1) and click\n**Load** - every matching sender is appended to the target list (duplicates\nskipped). From there you delete or move them with the normal tools (dry-run, move,\nexpunge...). This closes the loop: **AI finds the junk -\u003e you act on it precisely.**\n\n---\n\n## Bulk unsubscribe from newsletters\n\nOne of the most useful things you can do from the **Spam addresses** tab: stop the\nnewsletters at the source. Many bulk senders include a `List-Unsubscribe` header;\nthe tool captures it during an AI report and lets you **unsubscribe from the\nselected senders in one go** - using the same row checkboxes / *select all* as the\nother bulk actions. It is **not** a magic \"100% one-click\", because the standard\nallows different mechanisms:\n\n- **`mailto:`** → unsubscribe by **sending an email** to the listed address. Fully\n  **automatic** (sent from your **active SMTP profile** in the Notifications tab).\n- **HTTPS one-click** (RFC 8058, the sender advertises `List-Unsubscribe-Post`) →\n  a single **HTTPS POST**. Fully **automatic**.\n- **Plain HTTPS link** (no one-click) → usually a **confirmation page** that\n  **can't be automated**; you finish it by hand in the browser.\n\nSo the result is **automatic for most, plus open-the-page for the rest**. The\n**Unsub** column shows the state per sender:\n\n- **`✓ done`** - already unsubscribed; hover for the **method, date and result** of\n  the request (this is recorded per sender once an automatic unsubscribe succeeds).\n- **`auto ✉`** - automatic via a `mailto:` (an email sent from your active SMTP\n  profile). **`auto`** - automatic via a one-click HTTPS request (no SMTP needed).\n- **`link ↗`** - a confirmation page you open by hand.\n- **`rescan`** - a `List-Unsubscribe` was seen but no usable link is stored **yet**;\n  just **run a fresh AI report** and it is normally captured. If it still persists,\n  **clear the local cache** (connection card) and run the report again.\n- **`none`** - no `List-Unsubscribe` at all, so the sender can't be unsubscribed\n  from here (you can still flag or remove it). Only senders that actually have the\n  header can be unsubscribed.\n\nIf your selection includes senders you already unsubscribed (**`✓ done`**), it first\nasks whether to **re-do** them (e.g. if the first attempt didn't work) or **skip**\nthem. If any selected sender can only be unsubscribed by **email** but you have no\nactive SMTP profile, a banner points you to the **Notifications** tab to set one up.\nAfter\nthe action you get a summary (*N unsubscribed automatically, M need a manual page,\nK failed*). Rather than blasting dozens of browser tabs (pop-up blockers eat them\nanyway), the list\nthen **filters itself to the manual ones** so they are the only rows left - open\neach with its per-row **`link ↗`**. You can reach that view any time with the\n**Unsub filter** at the top of the tab (`all` / `auto` / `manual` / `none`).\n\n\u003e ⚠️ This makes **outbound requests** (an email and/or web POST/GET), so it's a\n\u003e deliberate step. Use it for **newsletters** (legitimate senders with a\n\u003e `List-Unsubscribe`), **not** for real spam - unsubscribing from actual spam just\n\u003e confirms your address is live. `mailto:` unsubscribes need an **active SMTP\n\u003e profile** configured in the Notifications tab.\n\n---\n\n## Gmail notes\n\n1. Enable 2-Step Verification, then create an **App Password** and use it\n   instead of your normal password.\n2. Enable IMAP in Gmail settings.\n3. Host is `imap.gmail.com`. Folder names are special: `[Gmail]/Trash`,\n   `[Gmail]/All Mail`, `[Gmail]/Spam` (localised, e.g. `[Gmail]/Cestino`).\n4. Use `--gmail-trash`: a plain delete in `INBOX` only removes the label, not\n   the message. Target `[Gmail]/All Mail` to catch archived mail too.\n5. **Trash is not permanent deletion.** On Gmail, deleting (including AI Cleanup)\n   moves mail to `[Gmail]/Trash`; it stays there until the Trash is emptied. The\n   web UI shows a reminder after any run that trashed mail and offers to select\n   the Trash folder with **Empty folder** ticked - press Run to remove it for\n   good. On the CLI, run `--empty-folder` against `[Gmail]/Trash` (or wait for\n   Gmail's automatic 30-day purge).\n\n---\n\n## Support\n\nQuestions, bugs, or feature ideas? Open an\n[issue](https://github.com/mrpickles007/imap-cleanup-tool/issues) or email\n**support@imapcleanuptool.com**.\n\n---\n\n## License\n\n**GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)** - see\n[LICENSE](LICENSE).\n\nThis is free, open-source software with a strong copyleft: you may use, study,\nmodify, and redistribute it, but **any derivative work - including software that\nreuses any part of this code, and modified versions offered over a network as a\nservice - must also be released as open source under the AGPL-3.0**. You cannot\nincorporate this code into a closed-source or proprietary product.\n\nContributions are welcome - see [CONTRIBUTING.md](CONTRIBUTING.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrpickles007%2Fimap-cleanup-tool","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrpickles007%2Fimap-cleanup-tool","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrpickles007%2Fimap-cleanup-tool/lists"}