Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3f7ef6f
refactor: use platformdirs for data storage and allow setup mode
karan-vk Feb 12, 2026
371c446
ci: add github actions build workflow
karan-vk Feb 12, 2026
38c3446
chore: update gitignore for test and tool artifacts
karan-vk Feb 12, 2026
72decff
feat: implement setup api endpoint for credentials upload
karan-vk Feb 12, 2026
ca2cd94
feat: add frontend setup view for missing credentials
karan-vk Feb 12, 2026
9a5a0e8
docs: add agent documentation and analysis files
karan-vk Feb 12, 2026
518c4e1
ci: remove macos-13 runner due to configuration error
karan-vk Feb 12, 2026
58adc1f
ci: add auto-release on tag push
karan-vk Feb 12, 2026
2dac02c
docs: update README with standalone app capabilities
karan-vk Feb 12, 2026
6fafb57
ci: optimize build workflow and update release action
karan-vk Feb 12, 2026
68fa67b
docs: fix formatting and language identifiers in local AGENTS.md guides
karan-vk Feb 12, 2026
aa9dc1c
docs: update main AGENTS.md with platformdirs and setup route
karan-vk Feb 12, 2026
3bd2214
refactor: sort API router exports alphabetically
karan-vk Feb 12, 2026
ecedcf4
fix(backend): security improvements and robust setup logic
karan-vk Feb 12, 2026
f319145
fix(build): enhance cross-platform packaging
karan-vk Feb 12, 2026
0eaa416
fix(frontend): improve file upload UX and validation
karan-vk Feb 12, 2026
497f57a
docs: update README based on CodeRabbit feedback
karan-vk Feb 12, 2026
0499370
docs: add community governance and improve documentation
karan-vk Feb 12, 2026
7590827
chore: remove AGENTS.md from git tracking
karan-vk Feb 12, 2026
735ccbc
feat(ui): redesign setup flow with multi-step wizard
karan-vk Feb 12, 2026
bb87e8f
feat(core): add custom exception hierarchy and error handling utilities
karan-vk Feb 12, 2026
7202818
feat(config): add performance settings for large inbox optimization
karan-vk Feb 12, 2026
1e05128
feat(scan): implement streaming mode for large inbox support
karan-vk Feb 12, 2026
a27f36c
refactor(gmail): integrate error handling decorators across services
karan-vk Feb 12, 2026
f97fbd1
refactor(api): improve error responses with structured error codes
karan-vk Feb 12, 2026
e0e1d82
feat(ui): add toast notification system for user feedback
karan-vk Feb 12, 2026
2371c45
refactor(ui): integrate notifications into existing UI components
karan-vk Feb 12, 2026
7559984
test(integration): add comprehensive integration test suite
karan-vk Feb 12, 2026
321a2ce
docs(roadmap): mark in-progress items as completed
karan-vk Feb 12, 2026
33cec6b
docs(readme): update release artifact filenames to match CI output
karan-vk Feb 13, 2026
66f53ac
chore: move GMAIL_SERVICES_ANALYSIS.md to .github/agent/reference/
karan-vk Feb 13, 2026
79922a0
fix(build): correct python_multipart module name in hiddenimports
karan-vk Feb 13, 2026
e00e186
feat(ui): improve setup wizard with missing credential steps
karan-vk Feb 13, 2026
3327cf2
docs(ui): add setup video tutorial link to wizard
karan-vk Feb 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Build

on:
push:
branches: [ main ]
tags: [ 'v*' ]
pull_request:
branches: [ main ]

jobs:
build:
name: Build on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
permissions:
contents: write
strategy:
fail-fast: false
matrix:
# macos-13 is the last Intel runner
# macos-14 is Apple Silicon (M1)
os: [ubuntu-latest, windows-latest, macos-14]

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Set up Python
run: uv python install 3.12

- name: Install dependencies
run: uv sync

- name: Build with PyInstaller
run: uv run python build_app.py

- name: Zip Build Output (Unix)
if: runner.os != 'Windows'
run: |
cd dist
zip -r ../gmail-cleaner-${{ matrix.os }}.zip gmail-cleaner/

- name: Zip Build Output (Windows)
if: runner.os == 'Windows'
run: |
Compress-Archive -Path dist/gmail-cleaner -DestinationPath gmail-cleaner-${{ matrix.os }}.zip

- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: gmail-cleaner-${{ matrix.os }}
path: gmail-cleaner-${{ matrix.os }}.zip
if-no-files-found: error
retention-days: 30

- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: gmail-cleaner-${{ matrix.os }}.zip
Comment thread
coderabbitai[bot] marked this conversation as resolved.

5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ IMPROVEMENTS.md
htmlcov/
coverage.xml

# GitHub Copilot instructions (user specific)
.github/copilot-instructions.md
# PyInstaller
build/
dist/
80 changes: 80 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# AGENTS.md - Development Guide

**Generated:** Thu, Feb 12, 2026
**Branch:** main

## OVERVIEW
Privacy-focused Gmail cleanup tool using **FastAPI** (Python 3.9+) and **Vanilla JS**. Runs locally/Docker, no external database.

## STRUCTURE
```
.
├── app/ # FastAPI backend
│ ├── api/ # Routes (actions.py, status.py)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
│ ├── core/ # Config & State (Global variables)
│ ├── models/ # Pydantic schemas
│ └── services/ # Business logic
│ ├── auth.py # OAuth flow & Credential mgmt (God Object)
│ └── gmail/ # Gmail API operations (Scan, Delete, Archive)
├── static/ # Frontend (Vanilla JS)
│ ├── js/ # Logic (Module pattern, global namespace)
│ └── css/ # Styles
├── templates/ # HTML (Jinja2)
└── tests/ # Pytest suite
├── unit/ # Isolated tests (Mocked)
└── integration/ # [EMPTY] Missing integration tests
```

## WHERE TO LOOK
| Task | Location | Notes |
|------|----------|-------|
| **Auth/Login** | `app/services/auth.py` | Handles OAuth, tokens, local server. Complex. |
| **Email Logic** | `app/services/gmail/` | Modular operations (scan, delete, etc.). |
| **Frontend UI** | `static/js/` | Ad-hoc framework. `main.js` orchestrates. |
| **State** | `app/core/state.py` | Global thread-safe state for async tasks. |
| **API Routes** | `app/api/` | `actions.py` (POST), `status.py` (GET), `setup.py` (Setup). |
| **Config** | `app/core/config.py` | Settings via Pydantic (`.env`). |

## CODE MAP (Key Symbols)
| Symbol | Type | Location | Role |
|--------|------|----------|------|
| `GmailService` | Class | `app/services/auth.py` | Wrapper for Gmail API resource. |
| `get_gmail_service` | Func | `app/services/auth.py` | **CRITICAL**. Retrieves/refreshes auth. |
| `process_scan` | Func | `app/services/gmail/scan.py` | Core scanning logic. |
| `GmailCleaner` | Obj | `static/js/main.js` | Global frontend namespace (State/UI). |
| `app` | Var | `app/main.py` | FastAPI application instance. |

## CONVENTIONS
- **State**: Backend uses `app.core.state` global variables for progress tracking.
- **Frontend**: "Module Pattern" with global `GmailCleaner`. No build step.
- **Async**: Backend is async (FastAPI), but uses `threading` for background tasks.
- **Testing**:
- `pytest` with `pytest-asyncio`.
- **Mocks**: Aggressive mocking of `credentials.json` in `conftest.py`.
- **Fixtures**: `mock_gmail_auth` (autouse) prevents real auth in tests.

## ANTI-PATTERNS (THIS PROJECT)
- **NO SECRETS**: NEVER commit `credentials.json`, `token.json`, or `.env`.
- **No `try/except`**: Use `HTTPException` or custom exceptions.
- **Frontend Coupling**: Logic/UI tightly coupled in `labels.js`/`delete.js`.
- **Global State**: Frontend relies on global `window.GmailCleaner`.

## COMMANDS
```bash
# Dev
uv sync
uv run python main.py

# Test
uv run pytest
uv run pytest tests/unit/services/auth/

# Lint
uv run ruff check .
uv run pyright
```

## NOTES
- **Auth Flow**: Uses a local server callback. Docker requires port mapping.
- **Data Persistence**: `token.json` stored in `./data/` (Docker volume) or platform-specific data dir via `platformdirs`.
- **Concurrency**: `app/services/gmail` uses batch requests (performance).
38 changes: 38 additions & 0 deletions GMAIL_SERVICES_ANALYSIS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Deep Dive Analysis: `app/services/gmail`

## 1. Overview
The `app/services/gmail` directory is well-structured and modular. It follows a clear separation of concerns, with each file handling a specific aspect of Gmail operations (scanning, deleting, archiving, etc.). The `__init__.py` file acts as a clean facade, exposing necessary functions while hiding internal implementation details.

## 2. Pattern Consistency
The codebase demonstrates high consistency in the following areas:

* **State Management**: All modules consistently use the global `state` object from `app.core.state` to track progress, store results, and handle errors. This ensures a unified way of communicating status to the frontend.
* **Service Retrieval**: Every module retrieves the Gmail service using `get_gmail_service()` from `app.services.auth`, handling potential errors in a uniform way.
* **Batch Processing**: Operations like scanning and deleting consistently use Gmail API's batch capabilities (`new_batch_http_request` or `batchModify`) to optimize performance and respect rate limits.
* **Status Reporting**: Each module provides a `get_<operation>_status()` function that returns a copy of the relevant state, maintaining a consistent interface for status polling.
* **Error Handling**: `try-except` blocks are used consistently to catch exceptions, update the state with the error message, and mark the operation as done.

## 3. Shared Helpers
`app/services/gmail/helpers.py` is effectively used to share common logic, preventing code duplication. Key helpers include:
* `build_gmail_query`: Centralizes query construction logic.
* `get_sender_info` & `get_subject`: Standardizes header parsing.
* `get_unsubscribe_from_headers`: Encapsulates the logic for finding unsubscribe links.
* `validate_unsafe_url`: Provides shared security validation.

## 4. "God Objects" & Complexity
While most files are focused, a few areas show signs of high complexity:

* **`app/services/auth.py` (External to `gmail` dir but critical)**: This file is a "God Object" candidate (685 lines). The `get_gmail_service` function contains a massive nested `run_oauth` function that handles the entire OAuth flow, including starting a local HTTP server. This logic should ideally be extracted into a separate `OAuthManager` class or module.
* **`app/services/gmail/delete.py`**: This file (439 lines) handles both the *scanning* of senders for deletion and the *actual deletion* (both single sender and bulk). Splitting this into `delete_scan.py` and `delete_action.py` would improve maintainability and separation of concerns.

## 5. Circular Dependencies
**Status: Clean.**
No circular dependencies were found. The dependency graph is unidirectional and healthy:
* `app/services/gmail/*` imports `app.core.state`, `app.services.auth`, and `app.services.gmail.helpers`.
* `app/services/auth` imports `app.core.state` and `app.core.settings`.
* `app/core/state` has no internal dependencies.
* `app/services/gmail/__init__.py` imports from submodules but submodules do not import back from `__init__`.

## 6. Recommendations
1. **Refactor `app/services/auth.py`**: Extract the OAuth flow logic into a dedicated handler to reduce the size and complexity of the auth service.
2. **Split `app/services/gmail/delete.py`**: Separate the scanning logic (`scan_senders_for_delete`) from the deletion logic (`delete_emails_by_sender`, `delete_emails_bulk`) to align with the single-responsibility principle.
46 changes: 44 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ A **free**, privacy-focused tool to bulk unsubscribe from emails, delete emails

> **No Subscription Required - Free Forever**

## 🚀 Download & Install

The easiest way to use Gmail Cleaner is to download the standalone app for your operating system.

1. Go to the [Releases](https://github.com/Gururagavendra/gmail-cleaner/releases) page.
2. Download the version for your OS:
* **Windows**: `.exe` installer
* **macOS**: `.dmg` or `.app`
* **Linux**: `AppImage` or binary
3. Launch the app and follow the on-screen instructions to upload your `credentials.json`.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
## Features

| Feature | Description |
Expand Down Expand Up @@ -76,6 +87,10 @@ Lets make this tool a better one by improving as much as possible, All features

**Important**: You must create your **OWN** Google Cloud credentials. This app doesn't include pre-configured OAuth - that's what makes it privacy-focused! Each user runs their own instance with their own credentials.

Once you have your `credentials.json`, you can either:
- **Standalone App**: Simply upload it via the app's user interface.
- **Docker/Python**: Place it in the project root folder.

### 1. Get Google OAuth Credentials

**Video Tutorial**: [Watch on YouTube](https://youtu.be/CmOWn8Tm5ZE) for a visual walkthrough
Expand Down Expand Up @@ -129,7 +144,14 @@ cd gmail-cleaner

## Usage

### Option A: Docker (Recommended)
### Option A: Standalone App (Recommended)

1. Download and install the app from the [Releases](https://github.com/Gururagavendra/gmail-cleaner/releases) page.
2. Launch the application.
3. When prompted, upload your `credentials.json` file.
4. Click **"Sign In"** and follow the OAuth flow in your browser.

### Option B: Docker (Alternative / Server Mode)

1. Pull the latest image and start the container:
```bash
Expand Down Expand Up @@ -193,7 +215,7 @@ rm -f ./data/token.json
docker compose up
```

### Option B: Python (with uv)
### Option C: Python (Development)

```bash
uv sync
Expand Down Expand Up @@ -392,6 +414,26 @@ This error occurs when you try to use an **IP address** in the redirect URI (e.g

**Remember:** The redirect URI in Google Cloud Console must exactly match what you set in `OAUTH_HOST` + port.

## 🛠️ Development & Building

If you want to build the standalone app yourself:

1. Install [Python 3.9+](https://www.python.org/downloads/) and [uv](https://docs.astral.sh/uv/).
2. Clone the repository:
```bash
git clone https://github.com/Gururagavendra/gmail-cleaner.git
cd gmail-cleaner
```
3. Install dependencies:
```bash
uv sync
```
4. Build the app:
```bash
uv run python build_app.py
```
The built application will be available in the `dist/` folder.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
## Contributing

PRs welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) first.
Expand Down
3 changes: 2 additions & 1 deletion app/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

from .status import router as status_router
from .actions import router as actions_router
from .setup import router as setup_router

__all__ = ["status_router", "actions_router"]
__all__ = ["actions_router", "setup_router", "status_router"]
69 changes: 69 additions & 0 deletions app/api/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
Setup API Routes
----------------
Endpoints for initial application setup (uploading credentials).
"""

import json
import logging
import os

from fastapi import APIRouter, File, HTTPException, UploadFile, status

from app.core import settings

router = APIRouter(prefix="/api", tags=["Setup"])
logger = logging.getLogger(__name__)


@router.post("/setup")
async def setup_credentials(file: UploadFile = File(...)):
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"""Upload credentials.json file."""
# Check if credentials already exist
if os.path.exists(settings.credentials_file):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Credentials file already exists. Please delete it manually to upload a new one.",
)

try:
content = await file.read()

# Validate JSON structure
try:
data = json.loads(content)
except json.JSONDecodeError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid JSON file",
)

# Validate content (must be Google OAuth credentials)
if "installed" not in data and "web" not in data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid credentials file. Must contain 'installed' or 'web' client configuration.",
)

# Ensure directory exists
os.makedirs(
os.path.dirname(os.path.abspath(settings.credentials_file)), exist_ok=True
)

# Save to settings.credentials_file
with open(settings.credentials_file, "wb") as f:
f.write(content)

logger.info(f"Credentials uploaded successfully to {settings.credentials_file}")
return {
"message": "Credentials uploaded successfully",
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

except HTTPException:
raise
except Exception as e:
logger.exception("Error uploading credentials")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to upload credentials. Please check server logs.",
) from e
Loading