Build Clean Email

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.

A mailbox cleanup tool built around the fact that its own actions are the risk. Bulk operations are previewed and reversible, rules are simulated before being enabled, and unsubscribing prefers the mechanism that does not tell a spammer you exist.

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

AccessMail provider APIs with the narrowest scope that permits the operationsGroupingClustering by sender, list identifier and subject patternSafetyMove to a holding label with retention, never deleteRulesSimulated against the mailbox before enablingUnsubscribeHeader mechanism preferred, links treated as unsafe by default

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 grouping queries.

    Get it Docker or your package manager. open ↗

    Verify psql connects and groups a hundred thousand rows quickly

  3. free

    Why Grouping, rotating sender addresses and the scale problems only appear in a mailbox with years of accumulated mail.

    Get it Your own account, or a test account you have deliberately filled. Read the provider's scope documentation before designing the consent screen.

    Verify An account with at least fifty thousand messages syncs metadata

  4. have readyfree

    Why This build's own actions are the risk it carries, and testing bulk operations against a real mailbox without a backup is how you lose correspondence.

    Get it Export the mailbox through the provider's own export before running anything destructive.

    Verify A complete export exists and has been checked for size and message count

  5. decidefree

    Why It is the window in which a mistake is recoverable, and it is enforced in code, so it belongs before the first bulk action.

    Get it Decide how long moved mail is kept before actual deletion, and whether actual deletion happens at all.

    Verify A written note states the period and whether permanent deletion is ever performed

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)
- messages(id, account_id, provider_id, sender, list_id, subject, received_at, size_bytes, thread_id) · metadata only
- groups(id, account_id, kind, key, message_count, oldest_at, newest_at) · kind is sender, list or subject_pattern
- actions(id, account_id, kind, group_id, matched_count, previewed_count, executed_at, undone_at) · kind is move, label or unsubscribe
- moved(action_id, provider_id, from_folder, to_folder, at) · exactly what moved where, so undo is real
- rules(id, account_id, condition_json, action, enabled, simulated_count, simulated_at)
- unsubscribes(id, group_id, method, attempted_at, outcome, safe) · method is header or link, and link is unsafe by default

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/mailcleanMessage metadata. Senders and subjects reveal a great deal even without bodies.
STORE_MESSAGE_BODIESrequiredneverPresent as a constant so the answer is visible. There is no other value.
HOLDING_LABELrequiredCleanup HoldingWhere bulk actions move mail. Nothing is deleted.
HOLDING_RETENTION_DAYSrequired60The window in which a mistake is recoverable.
RULE_VOLUME_CAP_PER_DAYrequired500How much one rule may act on before pausing. Bounds a mistake.
FOLLOW_UNSUBSCRIBE_LINKSrequirednever_automaticallyClicking a link in unsolicited mail confirms a person is there.

The build, in order

  1. Seeing the mailbox without reading it

    You can understand a large mailbox from metadata alone.

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

      Files src/oauth.ts

    2. Sender, subject, size, dates. Assert no bodies in the suite.

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

    3. Hundreds of thousands of messages. A restart from zero is unacceptable.

      Files src/sync.ts

    done when · tick each as it passes
    watch out
    • Requesting full read access because it is the simplest scope means your application holds the ability to read every message the user has ever received, which is a liability far exceeding what cleanup requires. Request what the operations need, store metadata only, and assert in the suite that no body text is persisted.
  2. Grouping so cleanup is possible at all

    A hundred thousand messages become a few hundred decisions.

    1. It is the field designed for exactly this and it survives address rotation.

      Files src/group.ts

    2. So one newsletter is one decision rather than fifty.

      Files src/group.ts

    3. Size is what motivates cleanup. Range is what makes it safe.

      Files src/groups.ts

    done when · tick each as it passes
    watch out
    • Grouping by exact sender address fragments a single newsletter into fifty groups when the sender rotates its envelope address, which is common, and the user gives up. Use the list identifier where present and fall back to a normalised sending domain, so one newsletter is one decision.
  3. Bulk actions you can take back

    Every destructive action is previewed and reversible.

    1. The exact number, and messages the user can look at.

      Files src/preview.ts

    2. Deletion is the risk. Retention makes it reversible for a while.

      Files src/actions.ts

    3. So undo restores rather than piling everything in one folder.

      Files src/moved.tstests/undo.test.ts

    done when · tick each as it passes
    watch out
    • Recording only that an action ran, without the per-message origin, makes undo a best guess that puts everything in one folder, which is not a restoration and is sometimes worse than the original mistake. Record the source folder per message, so undo is exact.
  4. Rules, simulated before they run

    A rule cannot surprise you after it is enabled.

    1. The count against real mail is the only useful preview.

      Files src/simulate.ts

    2. An edited rule is a new rule.

      Files src/rules.ts

    3. So a mistake is bounded rather than open ended.

      Files src/rules.ts

    done when · tick each as it passes
    watch out
    • A rule enabled without simulation runs against every future message and the user finds out months later that something important has been filed away since April. Simulate against history, show the count, and cap the volume a single rule may act on so a mistake is bounded.
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.