A Go-based web application that helps you clean up your Fastmail inbox by finding and archiving similar emails using JMAP protocol.
- Safe Operations: Built-in dry run mode prevents accidental changes
- Dual-pane Interface: View inbox on left, grouped similar emails on right
- Smart Similarity Matching: Fuzzy matching based on subject, sender, and email content
- Adjustable Similarity Threshold: Fine-tune matching with a percentage slider
- Selective Archiving: Choose which emails to archive with confirmation dialog
- Rapid Archive Loop: "Archive & Find Next" button archives the current selection without confirmation and immediately re-runs the similarity search
- Undo Archive: After each successful archive a status pill with an "Undo" button appears in the top-right of the action bar to restore the just-archived messages
- Individual Email Selection: Select specific emails to find similar matches
- Mock Mode: Run against built-in sample data — no Fastmail credentials required
- DRY RUN MODE: All write operations are disabled by default; archiving then only says how many emails it would have archived
- Archive Only: The only write operation is moving emails to archive (never deletes); any other labels on an email are kept
- Confirmation Dialog: Required before archiving via "Archive Selected" (the opt-in "Archive & Find Next" button intentionally skips it)
- Undo Archive: An "Undo" button appears after each successful archive and moves the affected messages back to the inbox via
POST /api/unarchive - Visual Warnings: Clear indication when in dry run mode
- Flagged Emails Stay: Emails you flagged in Fastmail start unselected in a group of similar emails, so archiving the group leaves them in your inbox unless you select them
- Go 1.26 or later
- Fastmail account with JMAP access
- Fastmail API token (generated from account settings)
-
Clone/download the project
-
Install dependencies:
go mod download
-
Create your configuration file:
cp config.yaml.example config.yaml
-
Edit
config.yamlwith your Fastmail API token:jmap: endpoint: "https://api.fastmail.com/jmap/session" api_token: "your-api-token-here" # IMPORTANT: Set to false only when ready for real changes dry_run: true
- Log into your Fastmail account
- Go to Settings → Privacy & Security → Integrations
- Click "New API Token"
- Set the scope to "Mail" access
- Generate the token and copy it to your config file
-
Start the server:
go run main.go
-
Open your browser to: http://localhost:8080
-
The application will display a warning banner when in dry run mode
To build a single binary instead, run go build -o mailboxzero . The web page is built into it, so you can run it from any directory: ./mailboxzero -config /path/to/config.yaml.
For development, testing, or demos you can run against built-in sample data instead of a real Fastmail inbox:
cp config-mock.yaml.example config.yaml
go run main.goMock mode is enabled by setting mock_mode: true in the config; when on, the JMAP endpoint and API token are not required. Keep dry_run: true here too — archive operations simulate against the in-memory sample emails.
- Load Inbox: The left pane shows your current inbox emails
- Find Similar Emails:
- Click "Find Similar Emails" to find all similar email groups (it searches the newest 10,000 emails in your inbox)
- Or select a specific email and click "Find Similar Emails" to find matches for that email
- Adjust Similarity: Use the percentage slider to fine-tune matching sensitivity
- Review Matches: Similar emails appear in the right pane with checkboxes
- Select for Archiving: Choose which emails to archive (all selected by default, except emails you flagged, marked ⚑; unread emails have a blue dot)
- Archive: Click "Archive Selected" and confirm to move emails to archive folder, or click "Archive & Find Next" to archive immediately without confirmation and automatically search for the next batch of similar emails
- Similarity Slider: Adjust from 0-100% to control how strict the matching should be
- Select All/None: Quickly select or deselect all found similar emails
- Individual Selection: Click on specific emails to select/deselect them
- Clear Results: Remove all results from the right pane to start fresh
| Key | Action |
|---|---|
F |
Find Similar Emails |
A |
Archive & Find Next (archives at once, without the confirmation dialog) |
U |
Undo the last archive |
R |
Refresh the inbox |
C |
Clear Results |
Escape |
Close the preview or the archive dialog |
↑ / ↓ |
Move between emails in a list |
Enter / Space |
Choose the focused inbox email (to find emails similar to it) |
-
Edit
config.yaml:dry_run: false
-
Restart the application
-
The warning banner will disappear
-
Archive operations will now actually move emails
server:
port: 8080 # Web server port
host: "localhost" # Web server host: localhost, 127.0.0.1 or ::1 only
jmap:
endpoint: "https://api.fastmail.com/jmap/session"
api_token: "" # Your Fastmail API token
dry_run: true # Safety feature - set to false to enable changes
default_similarity: 60 # Default similarity percentage (0-100)
mock_mode: false # Set to true to use built-in sample data (no JMAP needed)The application uses weighted scoring tuned for newsletter and notification clustering:
- Sender Similarity (50%) — Structured ladder, not raw string distance. Same full address → 1.0; same domain → 0.8 (suppressed for shared-ESP domains like
sendgrid.netunless local parts also match); same registrable root domain (e.g.mail.google.com↔accounts.google.com) → 0.7; same specific display name → 0.6. - Subject Similarity (30%) —
max(token Jaccard, Levenshtein)over a normalized subject with reply/forward prefixes (Re:,Fwd:,[list]) stripped. Common newsletter stop words (weekly,newsletter,update, etc.) are filtered before Jaccard. - Content Similarity (20%) — Jaccard overlap of normalized word tokens (length ≥ 3) from the email's preview text. Full bodies are not downloaded for matching; the app fetches an email's body only when you hover over it to preview it.
Clusters are formed by single-link expansion: an email joins a group if it matches any existing member at the threshold, not just the seed — so a sibling that diverges from the group's first email but matches another member still gets pulled in.
To stay fast on large inboxes, grouping only compares emails that could reach the threshold: above 50% only emails whose senders have something in common, and between 30% and 50% also emails whose preview text shares rare words. At 30% and below every pair is compared, so very low thresholds are slower on a big inbox.
- Localhost Only: The web UI has no login, so the server only listens on this machine.
server.hostmust belocalhost,127.0.0.1or::1; anything else (including0.0.0.0) is rejected at startup. To use it from another machine, forward the port over SSH (ssh -L 8080:localhost:8080 your-server) and open http://localhost:8080 locally - Browser Protections: Other websites open in your browser cannot use the app: requests addressed to any host name but localhost are refused (blocking DNS rebinding), and cross-origin
POSTrequests are refused (blocking cross-site request forgery) - API Tokens: Use Fastmail API tokens for secure authentication
- Local Only: All processing happens locally - no data sent to external servers
- Read-Heavy: Only reads email data, minimal write operations
- Archive Only: Never deletes emails, only moves them to archive
- Verify your Fastmail API token is correct
- Ensure JMAP is enabled in your Fastmail account
- Check network connectivity
- Verify you have emails in your inbox
- Check if your account has the expected mailbox structure
- Try adjusting the similarity threshold
- Some emails may have very little content to compare
- Ensure emails have sufficient text in subject/preview
The project structure:
mailboxzero/
├── main.go # Application entry point
├── config.yaml # Configuration file
├── internal/
│ ├── config/ # Configuration handling
│ ├── jmap/ # JMAP client implementation
│ ├── server/ # Web server and API handlers
│ └── similarity/ # Email similarity algorithms
└── web/
├── web.go # Builds templates and static files into the binary
├── templates/ # HTML templates
└── static/ # CSS and JavaScript files
The project includes comprehensive unit tests for all packages:
# Run all tests
go test ./...
# Run tests with verbose output
go test ./... -v
# Run tests with coverage
go test ./... -cover
# Generate coverage report
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.outEnd-to-end browser tests live in e2e/ and use Playwright with your installed Google Chrome. They start the server in mock mode on port 18080:
cd e2e
npm ci
npx playwright testThis project is licensed under the MIT License - see the LICENSE file for details.
