Build SaneBox

KINDAback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

An email triage system built around an asymmetric cost: a false positive that files something important is far worse than a false negative that leaves noise in the inbox, so the classifier, the digest and the reversibility all lean the same way.

estimated effort multi-daythe files for this build are in the project pack

AccessMail provider APIs with the narrowest scope that permits moving messagesSignalsSender history, your reply behaviour and thread participationClassificationA threshold tuned for precision on the leave-in-inbox sideReversalEvery move recorded and undoable, with training from the correctionDigestA scheduled summary of what was filed

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the server and the sync workers.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree locally

    Why Message metadata at mailbox scale with sender aggregation.

    Get it Docker or your package manager. open ↗

    Verify psql connects and aggregates two hundred thousand rows quickly

  3. free

    Why Sender profiles are built from your own replies over time, and a fresh account contains none of the signal the whole approach depends on.

    Get it Your own account, and read the provider's scope documentation to find the narrowest one that permits moving messages.

    Verify At least three years of history syncs as metadata

  4. have readyfree, and it takes an evening

    Why The two error rates cannot be measured without ground truth, and only you know which of your messages mattered.

    Get it Take a random sample from your own mailbox and label each as should have stayed or could have been filed. Do not use a public dataset.

    Verify Five hundred labelled messages exist with both classes represented

  5. decidefree

    Why The threshold is set from it, and choosing after seeing the results means choosing whichever produces a flattering figure.

    Get it Decide how often you could tolerate something important being filed, expressed as a rate, before measuring anything.

    Verify A number is written down and the threshold is derived from it

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- accounts(id, provider, address, scopes_granted, connected_at)
- senders(id, account_id, address_normalised, domain, first_seen_at, replied_count, ignored_count, filed_count)
- messages(id, account_id, provider_id, sender_id, subject, received_at, thread_id, was_replied) · metadata only
- decisions(id, message_id, action, confidence, signals_json, at, reversed_at, reversed_by)
- folders(id, account_id, name, kind, digest_schedule) · kind is later, newsletter, seen_it or blocked
- digests(id, account_id, period_start, period_end, sent_at, message_count)
- A decision records the signals that produced it, so a correction can be traced to the signal that was wrong rather than to a model that cannot be examined.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/triageMessage metadata and sender behaviour. Revealing even without bodies.
STORE_MESSAGE_BODIESrequiredneverPresent as a constant. Sorting a mailbox does not require reading it.
FILE_CONFIDENCE_THRESHOLDrequired0.95High. Uncertain means leave it in the inbox.
DIGEST_SCHEDULErequireddailyShort enough that a mistake is caught within a day.
DELETE_MESSAGESrequiredneverPresent as a constant. Nothing is ever deleted.
FOLLOWUP_DAYSrequired4Before reminding about an unanswered sent message.

The build, in order

  1. Sorting without reading

    Triage works from metadata and no message body is stored.

    1. And document what it grants. Broad access is a liability even unused.

      Files src/oauth.ts

    2. Assert it in the suite, including log paths.

      Files src/sync.tstests/no-bodies.test.ts

    3. Two hundred thousand messages is a normal mailbox.

      Files src/sync.ts

    done when · tick each as it passes
    watch out
    • Requesting full mailbox read access because it is the simplest scope gives your application the ability to read every message the person has ever received, which is a liability far exceeding what sorting requires and a permission prompt that loses users. Request the narrowest working scope and store metadata only.
  2. Signals from what you actually do

    The strongest signal is whether you reply, and it is used first.

    1. It is the only signal that is genuinely about you.

      Files src/signals.ts

    2. Not as unimportant. The default must be to leave it alone.

      Files src/signals.ts

    3. Newsletters rotate constantly and fragment otherwise.

      Files src/senders.ts

    done when · tick each as it passes
    watch out
    • Classifying on sender reputation lists or on subject keywords ignores the only signal that is actually about you, which is whether you have ever replied to this person. Build from your own history first, and treat an unknown sender as unknown rather than as unimportant.
  3. A threshold tuned for the expensive mistake

    The classifier is deliberately reluctant to file anything.

    1. Uncertain means leave it alone.

      Files src/classify.ts

    2. Five hundred messages you judged yourself. There is no substitute.

      Files eval/labelled.json

    3. A single accuracy figure hides the expensive one.

      Files src/evaluate.tstests/error-rates.test.ts

    done when · tick each as it passes
    watch out
    • Reporting a single accuracy figure hides the only number that matters, which is how often something important was filed, and a classifier at ninety five percent accuracy that is wrong in the expensive direction is far worse than one at eighty five that is not. Measure and report the two error rates separately.
  4. Nothing is hidden and nothing is deleted

    Filed messages are visible, reversible and summarised on a schedule.

    1. A folder nobody opens is a deletion with extra steps.

      Files src/folders.ts

    2. A mistake caught within a day is recoverable.

      Files src/digest.ts

    3. The correction is the most valuable signal you will get.

      Files src/reversal.ts

    done when · tick each as it passes
    watch out
    • Filing messages into a folder nobody opens is functionally the same as deleting them, and the person only discovers the important one when someone asks why they never replied. Send a digest on a schedule short enough that a mistake is caught within a day, and make it skimmable rather than complete.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more email.