Skip to main content

inbox-jev

Not an engineer? Read How Inbox Jev sorts your email: what it does to your inbox, how to correct it and what to do when it gets something wrong, in plain words.

Triage your Gmail inbox with TypeSafe Jev. Every few minutes it reads new inbox emails, asks Jev which category they belong to (with a calibrated confidence), and:

  • labels them (Jev/Important, Jev/Action, Jev/Marketing, …),
  • archives receipts, tool notifications, newsletters, marketing and cold sales outreach,
  • unsubscribes from verified marketing and cold-outreach sequences (RFC 8058 one-click or mailto:) and moves scams/phishing to Spam,
  • keeps what needs you in the inbox: personal email (Jev/Important) and automated email that needs an action (Jev/Action: approve a payment, update a card, verify a domain…),
  • leaves anything it is not sure about in the inbox with Jev/Review,
  • learns from your corrections (change a label or move an email back → sender rule),
  • sends a daily Slack digest of what it did.

Everything is recorded in a database (SQLite by default) so it can be audited and undone.

Categories and labels

The categories below are a starting point, not a fixed taxonomy: they come from categories.yaml and reflect one person's preferences (for example, a lead from your website's form or a meeting someone booked with you is an action; newsletters are archived). Tune the criteria, thresholds and actions, or add categories, in your own copy of the file, without editing the repo (Your own categories). Brand names in the criteria (HubSpot, Calendly…) are examples for Jev, not rules. The defaults:

Category Label Action min_confidence
important Jev/Important keep —
action_required Jev/Action keep —
transactional Jev/Transactional archive 0.80
notification Jev/Notifications archive 0.80
newsletter Jev/Newsletters archive 0.80
marketing Jev/Marketing archive + unsubscribe 0.90
cold_outreach Jev/Outreach archive + unsubscribe (never from known contacts) 0.90
spam (scams, phishing) Jev/Spam move to Spam 0.90

Extra labels: Jev/Needs Reply (Jev thinks the sender expects a reply or a personal action, not a company survey, review request or promotion; only on emails that stay in the inbox), Jev/Invoices (tag: an invoice to pay), Jev/Review (low confidence) and Jev/Processed (technical marker).

How it decides

A plain-language overview is in docs/guide.md. The full functional spec, with flowcharts, decision tables, the exact checks and worked examples, is in docs/how-it-decides.md. The reasons behind each rule, and the alternatives that were rejected, are in docs/decisions/. In short, for each new inbox email:

  1. Allowlist (allow_domains / allow_senders in accounts.yaml): always important, never archived. Jev is asked only for "needs a reply" and the tags.
  2. Sender rules decide the category. They come from your corrections, from inbox-jev rules set, or are created automatically after 3 consistent answers at ≥ 0.95 in label/act (never in dry_run, never from a group archive, a known contact's pitch, or an answer with some keep probability, so senders that mix routine and action-needed mail keep going through Jev). Rules to archive or spam categories skip Jev; rules to keep categories still ask it for "needs a reply" and the tags, like the allowlist (if that call fails, the email is still labeled, just without those flags). A rule whose category was removed from categories.yaml is ignored (with a warning in the log): Jev decides, even when a @domain rule also matches. Senders in the account's decide_each_email (platforms that send both FYIs and mail that needs you, e.g. a CRM's notifications address that also relays your website's contact-form leads) never get a learned rule: Jev decides each of their emails, a correction fixes only that email, and only a manual rules set rule applies (ADR 0045). The same holds for an email sent as a mailing list's own address (its From is the address its List-Post or Google Groups' Mailing-list names, e.g. a group whose author can't be recovered): that address carries every member's mail (ADR 0051).
  3. Jev gets the headers that matter, the start of the body (BODY_CHARS; the HTML when the text/plain part is just a "view this in your browser" stub) and previous_contact: whether you ever wrote to the sender or a person at their domain, from a search over your sent mail (emails sent only to machine addresses, like noreply@ or a notification's …@bounce.linkedin.com reply address, don't count). It also gets three phishing signals, each only when it says something: a reply or forward subject with no In-Reply-To/References (reply_without_thread), the registrable domains the body's links point to (link_domains, never a full URL) and Gmail's SPF, DKIM and DMARC verdicts (sender_auth; for a trusted group's author, Google's ARC record of the message the group received, unknown without a trusted one, never the group's own pass). The spam criteria treat a pass as proof of the sending domain only and look for mismatches (§4.7, ADR 0042). It answers with a category and its probabilities, "needs a reply" and the tags.
  4. Policy turns the answer into actions, and the mode decides how much of that is applied:
    • at or above a category's min_confidence, its action applies;
    • below it, the email is still archived when Jev's top answer is an archive category and the archive categories together reach 0.90 (the archive group: never unsubscribes or spams; an email that "expects a reply" stays unless it's a stranger's pitch or a no-reply notification with no keep signal);
    • otherwise nothing destructive happens: Jev/Review plus the suggested label. Phishing that Jev calls spam below 0.90 lands here by design, already labeled Jev/Spam: the user removing Jev/Review (or changing another suggested label to Jev/Spam) makes a sender rule (the guide, operations).

An unsubscribe is irreversible, so it only happens in act mode (and act_categories), for marketing and cold_outreach at their own 0.90 confidence, when the sender passes DMARC (in Gmail's own Authentication-Results, parsed strictly per RFC 8601: a header that doesn't parse verifies nothing, §4.2) and sends a List-Unsubscribe header (links in the body are never followed), never through a group or discussion list, never for cold outreach from someone you have written to, never for a sender in decide_each_email unless a manual rule decides, once per list and at most MAX_UNSUBSCRIBES_PER_RUN per run. It uses RFC 8058 one-click, else mailto:, else leaves a manual link in the digest. Sender rules and inbox-jev unsubscribe-retry go through the same checks (§3.3; in plain language: What it unsubscribes you from). Google Groups on a domain you list in the account's group_domains (opt-in: only domains whose Google Workspace you control) are keyed on the author, so rules, corrections, the allowlist and previous_contact apply to the person (allowlist a group itself by its exact address in allow_senders); other groups stay keyed on the group address, which learns no rule from its mail (step 2), and an entry that is a public suffix (co.uk, github.io) fails the accounts file (§3.4).

Modes

MODE DB + digest Gmail labels archive / spam / unsubscribe
dry_run ✅ (learns no sender rules)
label ✅ ✅
act ✅ ✅ ✅

Start with dry_run or label for a week or two. Switch to act when the labels look right, all at once or category by category with act_categories in accounts.yaml (Rolling out act). Emails you restored or starred are left alone, and so are other edits to their Jev/ labels (one of ours removed, one we didn't set added). The three fixes the guide teaches (a category label swapped for another, Jev/Review removed, an archived email moved back to the inbox) are learned as a correction, and that correction then decides the email too (the guide).

Setup

uv tool install inbox-jev    # or: pipx install inbox-jev (Python 3.11+)
inbox-jev init               # guided setup, then an optional first dry run

An installed copy keeps its configuration and data in ~/.config/inbox-jev/ (.env, accounts.yaml, secrets/, data/); set INBOX_JEV_HOME to use another folder. Run from a checkout of this repo (uv sync --extra dev, then uv run inbox-jev init), it uses the checkout instead, as a server deployment does (ADR 0053).

inbox-jev init asks for what it needs, checks each value before saving it and keeps what is already configured, so running it again adds an account or fixes one step (ADR 0052):

  1. TypeSafe: the API key from the TypeSafe dashboard, checked with a request that classifies nothing, saved as TYPESAFE_API_KEY in .env (created from .env.example, owner-only).
  2. Google: Gmail access goes through an OAuth client of your own, in a Google Cloud project you control (inbox-jev ships none). The wizard opens the pages and walks through them: enable the Gmail API, set up the consent screen, create a Desktop app client and download its JSON, which it finds in ~/Downloads, checks and copies to secrets/client_secret.json.
  3. Slack (optional): a bot token (chat:write, plus users:read.email to find your member ID), checked and saved as SLACK_BOT_TOKEN.
  4. Accounts: for each Gmail address, the browser authorization (token → secrets/tokens/) and an entry in accounts.yaml with your digest's member ID and language and, for a company domain, that domain in allow_domains. The other options are in accounts.example.yaml.
  5. Schedule (optional): on the computer where it will run, a triage pass every 10 minutes and the digest daily at 08:00, with launchd (macOS) or systemd user units (Linux): inbox-jev schedule install. On Linux, systemd runs user timers only while you are logged in: on a headless machine, or to keep it running after you log out, enable lingering once (loginctl enable-linger $USER; install warns when it is off). Skip this step when you deploy to a server (Running it in production): two copies must never triage the same account, and install refuses on a server that runs the deployment's timer.

The categories are the defaults shipped with the package, updated with it. To tune them, copy them to categories.yaml in the configuration folder (or point CATEGORIES_FILE at your copy; Your own categories).

uv run pre-commit install adds gitleaks, ruff and mdformat before every commit if you develop.

The same, by hand
  • TypeSafe: API key from the TypeSafe dashboard → TYPESAFE_API_KEY. inbox-jev run fails each account's run without it (nothing is touched); the other commands don't need it.
  • Google:
    1. In Google Cloud Console, enable the Gmail API.
    2. Configure the OAuth consent screen (Google Auth Platform). Use Internal if all accounts are in your Workspace. Otherwise use External and publish the app (Audience → Publish app) so it is In production; it stays unverified, and Google shows a one-time warning when you authorize (Advanced → Go to your app). Don't leave it in Testing: Google expires a Testing app's tokens after 7 days, and every run then fails until you authorize again. When Google will end an authorization (a Testing app, or access you limited in time when allowing it), auth add, auth status and the digest warn you, with the fix (Gmail authorization).
    3. Create an OAuth client of type Desktop app and save its JSON as secrets/client_secret.json.
  • Slack (optional): a bot with the chat:write scope → SLACK_BOT_TOKEN. Add each person's member ID as slack_user_id in accounts.yaml.
cp .env.example .env                       # fill it in
cp accounts.example.yaml accounts.yaml     # your accounts + allowlists
uv sync --extra dev                        # exact versions from uv.lock
uv run pre-commit install                  # gitleaks, ruff, mdformat before every commit
uv run inbox-jev auth add you@example.com  # opens the browser; token → secrets/tokens/

Settings and both YAML files are checked when a command starts, so a typo stops it with a message instead of misbehaving later (Configuration checks).

Try it locally

uv run inbox-jev run --mode dry_run --limit 30
uv run inbox-jev stats
uv run inbox-jev digest --dry-print

Commands

Command What it does
inbox-jev init Guided setup: TypeSafe key, Google OAuth client, Slack, accounts, first dry run
inbox-jev schedule install / schedule status / schedule remove Run it on this computer: triage every 10 minutes, the digest daily at 08:00 (launchd or systemd user units)
inbox-jev auth add EMAIL Authorize a Gmail account (opens the browser; token → secrets/tokens/)
inbox-jev auth status [-a EMAIL] Which accounts are missing an authorization, or have one Google will end (a time-limited grant), with the fixes (Gmail authorization)
inbox-jev run [-a EMAIL] [--mode M] [--limit N] [--lookback 30d] One triage pass (what the timer runs)
inbox-jev digest [-a EMAIL] [--hours 24] [--dry-print [--blocks]] [--preview] Slack digest
inbox-jev restore -a EMAIL --since 1d [--category marketing] Undo: back to inbox, remove labels (emails deleted in Gmail are skipped)
inbox-jev reprocess -a EMAIL --category KEY [--low-confidence-only] Have the next act run redo that category's emails still in the inbox (pair with run --lookback)
inbox-jev rules list [-a EMAIL] [--all] / rules set -a EMAIL @domain.com newsletter / rules delete -a EMAIL SENDER Sender rules (rules set and rules delete take an address or @domain, any case; rules delete also removes a rule stored as typed before rules set checked its input)
inbox-jev retry-errors -a EMAIL Have the next run retry emails that failed, including those that stopped retrying after 8 failures (pair with run --lookback)
inbox-jev unsubscribe-retry [-a EMAIL] Retry failed unsubscribes after re-checking them against the current rules, corrections and unsubscribe checks
inbox-jev senders backfill [-a EMAIL] Store the author, not the group, for group emails saved before authors were recovered, and whether the sender is the list's own address
inbox-jev stats [-a EMAIL] [--days 7] Counts per category
inbox-jev labels rename OLD NEW [-a EMAIL] Rename a label in Gmail and the DB (see Renaming a label)
inbox-jev --version The release and, inside the image, the commit it was built from

Running it in production

On a server with Docker and systemd timers: a triage pass every 10 minutes, the daily digest, a staleness watchdog, nightly off-site backups and Slack alerts on failures. docs/operations.md has it all: deploy, updating a deployment and rolling back, rolling out act, changing categories, alerts, backups and the database.

The daily digest is one Slack DM per account, laid out with Slack's Block Kit: first how many emails need you and which (pending Jev/Action emails and replies, calendar invitations left out, how many are still in Review), then a table of what Jev did, unsubscribes done or waiting for a click, the corrections learned (sender and categories), and errors. What it shows, in plain words: the guide; schedule, language and preview: operations.

Privacy

Email headers and the first BODY_CHARS characters of each body are sent to the TypeSafe API, with the domains (never the URLs) of the links anywhere in the body and Gmail's SPF/DKIM/DMARC verdicts. Email bodies are not stored in the database or logged, only sender, subject and the classification. previous_contact runs Gmail searches over your sent mail and reads the To/Cc/Bcc headers (never the body) of matching sent emails: up to 10 for an address, up to 25 per run for a domain. Its result is logged on each "message processed" line but not stored in the DB. That line also carries timings_ms, the milliseconds each stage took (Gmail fetch, sender rule and sent-mail search, Jev, applying the decision): numbers only, no email content. For a domain, the search's progress is stored (sent_domain_scans): the domain, the dates and Gmail ids of the sent emails read at either end of the span, and the verdict; never addresses or content.

Development

scripts/check.sh runs what CI runs. The workflow (branch, PR, the QA ⇄ Dev loop, the hook, and the real-mail check for classification changes) is in docs/development.md; CLAUDE.md and .claude/ hold the Claude Code side of it.

License

MIT

Metadata

Release files for inbox-jev 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for inbox-jev 0.1.0
File Size Uploaded
inbox_jev-0.1.0.tar.gz 227.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for inbox-jev 0.1.0
File Interpreter ABI Platform
inbox_jev-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 467.8 kB

Release files / inbox_jev-0.1.0.tar.gz

Download URL inbox_jev-0.1.0.tar.gz
Size 227.4 kB
Tags Source
SHA-256 checksum
How to use checksums
97770e21cd4254e75bbb8ed0f536a282b23462174046d552fd554e69003c9377
BLAKE2b-256 checksum
How to use checksums
76d07c05f1582ed2286225c08ab3de099323093d5767a54942b22cba8ac127ce
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release files / inbox_jev-0.1.0-py3-none-any.whl

Download URL inbox_jev-0.1.0-py3-none-any.whl
Size 240.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2d951794ada6cd4d761dc57573343dff97fa64d0337626aa7dc12700f5265830
BLAKE2b-256 checksum
How to use checksums
73b51a0cfaf4d0d17981402a7479021dea2935b887565dd32195b7af06813a3a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page