A personal email screening utility using Himalaya, Jev, and Ratatui
Rust 100%
Just <1%

README.md

Screener #

Screener classifies inbox messages with Jev through TypeSafe, presents archive and delete recommendations in a Ratatui review screen, and applies only actions the user explicitly approves. Reviewer corrections can be retained as local lessons and supplied to future classifications.

screenshot

Dependencies #

  • A Rust toolchain with Cargo and Rust 2024 edition support.
  • just installed and available on PATH.
  • The himalaya CLI installed, on PATH, and configured with a default email account containing Inbox and Archive mailboxes.
  • A TypeSafe API key, supplied through TYPESAFE_API_KEY or --api-token-command.
  • Network access for TypeSafe API requests and the configured Himalaya backend.

Installation #

From the repository root:

just install

The install recipe runs cargo install --locked --force --path . and then writes an editable default policy to the user configuration directory. It never overwrites an existing default.json.

To install manually without just:

cargo install --locked --path .
screener --install-default-policy

When XDG_CONFIG_HOME is an absolute path, the installed policy is:

$XDG_CONFIG_HOME/screener/policies/default.json

Without XDG_CONFIG_HOME, Screener uses the platform configuration directory. Common locations are:

Linux:  ~/.config/screener/policies/default.json
macOS:  ~/Library/Application Support/screener/policies/default.json

The configured default.json takes precedence over the policy embedded in the binary. Re-running just install upgrades the binary while preserving user policy changes.

Policies #

Put named policies in the same policies directory:

$XDG_CONFIG_HOME/screener/policies/work.json
$XDG_CONFIG_HOME/screener/policies/newsletters.json

Select one by filename without the .json suffix:

screener --policy work
screener --policy newsletters

Explicit paths remain supported:

screener --policy ./policies/temporary-cleanup.json

--policy default selects the configured default.json, falling back to the embedded default when no configured file exists.

API credentials #

Set the key directly:

export TYPESAFE_API_KEY='...'
screener

Or obtain it from a password manager or another command:

screener --api-token-command 'pass show typesafe-token'

The command's trimmed standard output is used as the API key.

Selecting messages #

Screener processes the ten most recent messages by default:

screener
screener --count 100
screener --all

Use an age filter before reading bodies or classifying messages:

screener --older-than '1 week ago'
screener --count 25 --older-than '30 minutes ago'
screener --all --older-than '2 days ago'

Supported units are seconds, minutes, hours, days, and weeks. --count limits matching messages after age filtering.

Jev concurrency #

Screener reads message bodies first, then packs up to ten messages into each Jev request while keeping the serialized request under a conservative 24 KiB budget. This leaves headroom below Jev's documented context limits while preserving full message excerpts and relevant lessons whenever they fit. Results remain in inbox order regardless of completion order.

Increase or reduce the limit for the available network and TypeSafe API capacity:

screener --all --classification-concurrency 8

Accepted values are 1 through 16. Use 1 for serial classification if the API applies a strict rate limit.

If TypeSafe still reports max_tokens_exceeded, Screener transparently splits the affected batch and retries. For an oversized single-message request, it removes the least relevant historical lessons before shortening that message's submitted excerpt. A policy that cannot fit even with this reduced context fails locally with an actionable error instead of sending a known-oversized request.

TUI review #

The initial view shows archive and delete candidates. Every mailbox action defaults to keep.

Key Action
↑ / ↓, j / k Move through messages
← / h / n Keep the selected message
→ / l / y Apply the selected archive or delete action
Space Toggle keep/apply
a Apply all visible actionable recommendations
r Keep all visible messages
v Toggle action candidates/all classifications
t Teach a corrected classification
x Clear the selected pending lesson
Enter Finish and open final confirmation
q / Esc Cancel without mailbox changes or saved lessons

After pressing t, choose the corrected classification:

Key Classification
1 / r Reply
2 / v Review
3 / a Archive
4 / d Delete

Enter an optional note, then press Enter to retain the lesson. A correction to reply or review forces the current message to remain in the inbox. A correction to archive or delete changes the proposed action but still defaults to keep. The final y confirmation saves lessons and applies selected mailbox actions.

Reviewer lessons #

Lessons are separate from policy files. With an absolute XDG_CONFIG_HOME, they are stored at:

$XDG_CONFIG_HOME/screener/lessons.json

Otherwise Screener uses the platform-local data directory. Override the path with:

screener --lessons-file ./private-lessons.json

Each lesson contains a compact message excerpt, the original classification, the corrected classification, an optional note, and a policy scope. Relevant lessons are included in future Jev request state; they do not modify model weights or automatically rewrite a policy.

The lesson file contains private email excerpts. Keep it local and do not publish it.

Incorporating lessons into a policy with an agent #

Periodically use a coding agent to generalize repeated lessons into durable policy rules and examples. The goal is to encode category-level boundaries without copying personal message contents or sender-specific details into the policy.

A useful request is:

Read my Screener lessons file and the configured default policy. Group repeated
classification corrections into general rules, update the policy's decision order,
action indicators, exclusions, and examples, and preserve important counterexamples.
Do not copy personal data, message IDs, addresses, or message text into the policy.
Re-scope or remove migrated lesson records only after their behavior is represented
by the policy. Validate the JSON and evaluate representative corrected cases.

Files to provide to the agent:

$XDG_CONFIG_HOME/screener/lessons.json
$XDG_CONFIG_HOME/screener/policies/default.json

Review the proposed generalizations before accepting them. A lesson may represent a one-off preference rather than a safe global rule. Keep action-required mail, direct correspondence, and durable records as explicit counterexamples when adding broad deletion preferences.

Development checks #

just check

The check recipe runs the formatting check, test suite, and Clippy with warnings denied.