Vibecode Clean Email
track this build4 phases, 12 steps, beginner friendly0%The visible inbox cleanup loop is buildable, but a credible replacement needs more than the first screen. Clean Email earns its keep through deliverability, security, integration, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Clean Email. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Clean Email · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Access | Mail provider APIs with the narrowest scope that permits the operations | full mailbox access is a serious permission and this needs some of it | | Grouping | Clustering by sender, list identifier and subject pattern | cleaning by individual message does not scale to a real mailbox | | Safety | Move to a holding label with retention, never delete | the tool's own actions are the largest risk it carries | | Rules | Simulated against the mailbox before enabling | a rule that matches too much is discovered months later | | Unsubscribe | Header mechanism preferred, links treated as unsafe by default | clicking a link in spam confirms you exist | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the sync workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Message metadata at mailbox scale with grouping queries. - Get it: Docker or your package manager. - Verify: psql connects and groups a hundred thousand rows quickly - [ ] **A mail account with API access and a real history** · 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 - [ ] **A full backup of the mailbox you will test against** · free - 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 - [ ] **Your holding retention period** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Reading or analysing message content. - Broad provider coverage. - Spam classification. - Permanent deletion as a primary action. - sender reputation, deliverability, and suppression handling - provider-grade deliverability - push reliability across devices - team routing and shared inbox rules - abuse response and security operations If one of those is essential to you, that is the reason to keep paying for Clean Email, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Clean Email The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me bulk mailbox cleanup where every destructive action is reversible and previewed to replace Clean Email. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool deletes email in bulk, and email deletion is frequently irreversible. A rule that matches more than the user expected can remove years of correspondence in seconds, and the user will not find out for months. So: nothing is deleted. Everything moves to a holding label with a retention period, and actual deletion happens after that period or never. Every bulk action previews the exact count and a sample before running. Rules are simulated against the mailbox before they are enabled. And the tool records what it moved, so undoing is a real operation rather than a hope. The other trap is the unsubscribe button. Clicking an unsubscribe link in a message that was never legitimate confirms to the sender that a person read it, which produces more mail rather than less. Prefer the header-based unsubscribe mechanism where present, treat a link in an unrecognised message as unsafe, and say why rather than doing it quietly. ### Stack (fixed, do not substitute) - Access: Mail provider APIs with the narrowest scope that permits the operations · full mailbox access is a serious permission and this needs some of it - Grouping: Clustering by sender, list identifier and subject pattern · cleaning by individual message does not scale to a real mailbox - Safety: Move to a holding label with retention, never delete · the tool's own actions are the largest risk it carries - Rules: Simulated against the mailbox before enabling · a rule that matches too much is discovered months later - Unsubscribe: Header mechanism preferred, links treated as unsafe by default · clicking a link in spam confirms you exist ### Data model (create this before Phase 1) - 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 ### Phase 1 · Seeing the mailbox without reading it Build: Connect an account with the narrowest scope that permits the operations. Sync message metadata only, never bodies. Handle a mailbox with hundreds of thousands of messages without an unbounded initial sync. Trap: 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. Done when: Metadata for a large mailbox syncs incrementally without loading everything at once. No message body is stored anywhere, asserted in the test suite. The narrowest scope permitting the operations is requested and documented. An interrupted initial sync resumes rather than restarting. Do not build yet: No grouping, actions or rules. ### Phase 2 · Grouping so cleanup is possible at all Build: Cluster messages by sender, mailing list identifier and subject pattern. Show counts, date ranges and size per group. Handle a sender that uses many varying addresses for the same mail. Trap: 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. Done when: A newsletter using rotating sender addresses appears as one group, verified against a real mailbox. Groups show count, date range and total size. Subject-pattern grouping finds recurring automated mail that shares no sender. A hundred thousand messages produce a reviewable number of groups. Do not build yet: No actions, rules or unsubscribe yet. ### Phase 3 · Bulk actions you can take back Build: Preview the exact matched count and a sample before any bulk action. Move to a holding label with a retention period rather than deleting. Record exactly what moved from where, so undo restores each message to its original location. Trap: 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. Done when: A bulk action shows the exact count and a sample before executing. Nothing is deleted; messages move to a holding label. Undo restores every message to its original folder, verified after a move affecting several folders. The holding label's retention is stated and enforced. Do not build yet: No rules or unsubscribe yet. ### Phase 4 · Rules, simulated before they run Build: Rules matching on sender, list, subject and age. Simulate against the existing mailbox and show what would have matched before enabling. Re-simulate when a rule is edited. Cap how much one rule may act on in a period. Trap: 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. Done when: Enabling a rule requires viewing its simulation against the existing mailbox. Editing a rule re-simulates before saving. A rule matching more than its cap in a period pauses and reports rather than continuing. Rule actions are recorded and undoable like manual ones. Do not build yet: No unsubscribe or reporting yet. ### Phase 5 · Unsubscribing without making it worse Build: Prefer the header-based unsubscribe mechanism where a message provides one. Treat a link in an unrecognised or suspicious message as unsafe and explain why rather than following it. Verify afterwards whether mail actually stopped. Trap: Clicking an unsubscribe link in unsolicited mail confirms to the sender that a real person opened it, which reliably produces more mail rather than less. Use the header mechanism, which is machine-to-machine and does not signal engagement, and where only a link exists, say what the trade is instead of deciding for the user. Done when: The header mechanism is used where present. A link-only unsubscribe in an unrecognised message is marked unsafe with an explanation rather than followed automatically. Mail arriving after an unsubscribe is detected and reported. Blocking a sender is offered as the alternative when unsubscribing is unsafe. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading or analysing message content, which would require a far broader permission than cleanup needs. - Provider coverage beyond one or two, which is most of the maintenance the hosted products carry. - Spam classification, which the provider already does better and with far more signal than you have. - Permanent deletion as a primary action. Everything here moves and retains, deliberately. ### README must contain - That nothing is deleted, only moved to a holding label with a stated retention - That every bulk action previews the exact count and a sample first - That undo restores each message to its original folder, because the origin is recorded per message - That rules are simulated against the existing mailbox before they can be enabled - That link-based unsubscribes in unrecognised mail are treated as unsafe and why ===== AGENTS.md ===== # Agent instructions · Clean Email indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Mail provider APIs with the narrowest scope that permits the operations, Clustering by sender, list identifier and subject pattern, Move to a holding label with retention, never delete, Simulated against the mailbox before enabling, Header mechanism preferred, links treated as unsafe by default. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Clean Email 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Seeing the mailbox without reading it You can understand a large mailbox from metadata alone. ### Steps 1. Request the narrowest working scope And document what it grants. Broad access is a liability even unused. Files: `src/oauth.ts` 2. Store metadata only Sender, subject, size, dates. Assert no bodies in the suite. Files: `src/sync.ts`, `tests/no-bodies.test.ts` 3. Sync incrementally and resumably Hundreds of thousands of messages. A restart from zero is unacceptable. Files: `src/sync.ts` ### Done when - [ ] A large mailbox syncs incrementally - [ ] No message body is stored - [ ] The narrowest scope is requested and documented - [ ] An interrupted sync resumes ### 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. ## Phase 2 · Grouping so cleanup is possible at all A hundred thousand messages become a few hundred decisions. ### Steps 1. Group by list identifier first It is the field designed for exactly this and it survives address rotation. Files: `src/group.ts` 2. Fall back to a normalised domain So one newsletter is one decision rather than fifty. Files: `src/group.ts` 3. Show count, range and size Size is what motivates cleanup. Range is what makes it safe. Files: `src/groups.ts` ### Done when - [ ] A rotating-address newsletter forms one group - [ ] Groups show count, date range and size - [ ] Subject patterns find recurring automated mail - [ ] A large mailbox yields a reviewable group count ### 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. ## Phase 3 · Bulk actions you can take back Every destructive action is previewed and reversible. ### Steps 1. Preview count and sample first The exact number, and messages the user can look at. Files: `src/preview.ts` 2. Move to holding, never delete Deletion is the risk. Retention makes it reversible for a while. Files: `src/actions.ts` 3. Record per-message origin So undo restores rather than piling everything in one folder. Files: `src/moved.ts`, `tests/undo.test.ts` ### Done when - [ ] Actions preview exact count and a sample - [ ] Nothing is deleted, only moved to holding - [ ] Undo restores every message to its original folder - [ ] Holding retention is stated and enforced ### 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. ## Phase 4 · Rules, simulated before they run A rule cannot surprise you after it is enabled. ### Steps 1. Simulate against history before enabling The count against real mail is the only useful preview. Files: `src/simulate.ts` 2. Re-simulate on edit An edited rule is a new rule. Files: `src/rules.ts` 3. Cap per-rule volume So a mistake is bounded rather than open ended. Files: `src/rules.ts` ### Done when - [ ] Enabling requires viewing a simulation - [ ] Editing re-simulates before saving - [ ] A rule exceeding its cap pauses and reports - [ ] Rule actions are undoable like manual ones ### 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. ## Not in this build - Reading or analysing message content. - Broad provider coverage. - Spam classification. - Permanent deletion as a primary action. ## After v1, if you want it - Attachment-focused cleanup, since size is usually what prompts this - A scheduled review of what rules did last month, which is where trust is maintained - Sender reputation from your own behaviour, so groups you always read are never suggested ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Message metadata. Senders and subjects reveal a great deal even without bodies. DATABASE_URL=postgres://localhost:5432/mailclean # Required. Present as a constant so the answer is visible. There is no other value. STORE_MESSAGE_BODIES=never # Required. Where bulk actions move mail. Nothing is deleted. HOLDING_LABEL=Cleanup Holding # Required. The window in which a mistake is recoverable. HOLDING_RETENTION_DAYS=60 # Required. How much one rule may act on before pausing. Bounds a mistake. RULE_VOLUME_CAP_PER_DAY=500 # Required. Clicking a link in unsolicited mail confirms a person is there. FOLLOW_UNSUBSCRIBE_LINKS=never_automatically
You are building a lean indie version of Clean Email. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Clean Email · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Access | Mail provider APIs with the narrowest scope that permits the operations | full mailbox access is a serious permission and this needs some of it | | Grouping | Clustering by sender, list identifier and subject pattern | cleaning by individual message does not scale to a real mailbox | | Safety | Move to a holding label with retention, never delete | the tool's own actions are the largest risk it carries | | Rules | Simulated against the mailbox before enabling | a rule that matches too much is discovered months later | | Unsubscribe | Header mechanism preferred, links treated as unsafe by default | clicking a link in spam confirms you exist | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the sync workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Message metadata at mailbox scale with grouping queries. - Get it: Docker or your package manager. - Verify: psql connects and groups a hundred thousand rows quickly - [ ] **A mail account with API access and a real history** · 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 - [ ] **A full backup of the mailbox you will test against** · free - 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 - [ ] **Your holding retention period** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Reading or analysing message content. - Broad provider coverage. - Spam classification. - Permanent deletion as a primary action. - sender reputation, deliverability, and suppression handling - provider-grade deliverability - push reliability across devices - team routing and shared inbox rules - abuse response and security operations If one of those is essential to you, that is the reason to keep paying for Clean Email, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Clean Email The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me bulk mailbox cleanup where every destructive action is reversible and previewed to replace Clean Email. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool deletes email in bulk, and email deletion is frequently irreversible. A rule that matches more than the user expected can remove years of correspondence in seconds, and the user will not find out for months. So: nothing is deleted. Everything moves to a holding label with a retention period, and actual deletion happens after that period or never. Every bulk action previews the exact count and a sample before running. Rules are simulated against the mailbox before they are enabled. And the tool records what it moved, so undoing is a real operation rather than a hope. The other trap is the unsubscribe button. Clicking an unsubscribe link in a message that was never legitimate confirms to the sender that a person read it, which produces more mail rather than less. Prefer the header-based unsubscribe mechanism where present, treat a link in an unrecognised message as unsafe, and say why rather than doing it quietly. ### Stack (fixed, do not substitute) - Access: Mail provider APIs with the narrowest scope that permits the operations · full mailbox access is a serious permission and this needs some of it - Grouping: Clustering by sender, list identifier and subject pattern · cleaning by individual message does not scale to a real mailbox - Safety: Move to a holding label with retention, never delete · the tool's own actions are the largest risk it carries - Rules: Simulated against the mailbox before enabling · a rule that matches too much is discovered months later - Unsubscribe: Header mechanism preferred, links treated as unsafe by default · clicking a link in spam confirms you exist ### Data model (create this before Phase 1) - 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 ### Phase 1 · Seeing the mailbox without reading it Build: Connect an account with the narrowest scope that permits the operations. Sync message metadata only, never bodies. Handle a mailbox with hundreds of thousands of messages without an unbounded initial sync. Trap: 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. Done when: Metadata for a large mailbox syncs incrementally without loading everything at once. No message body is stored anywhere, asserted in the test suite. The narrowest scope permitting the operations is requested and documented. An interrupted initial sync resumes rather than restarting. Do not build yet: No grouping, actions or rules. ### Phase 2 · Grouping so cleanup is possible at all Build: Cluster messages by sender, mailing list identifier and subject pattern. Show counts, date ranges and size per group. Handle a sender that uses many varying addresses for the same mail. Trap: 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. Done when: A newsletter using rotating sender addresses appears as one group, verified against a real mailbox. Groups show count, date range and total size. Subject-pattern grouping finds recurring automated mail that shares no sender. A hundred thousand messages produce a reviewable number of groups. Do not build yet: No actions, rules or unsubscribe yet. ### Phase 3 · Bulk actions you can take back Build: Preview the exact matched count and a sample before any bulk action. Move to a holding label with a retention period rather than deleting. Record exactly what moved from where, so undo restores each message to its original location. Trap: 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. Done when: A bulk action shows the exact count and a sample before executing. Nothing is deleted; messages move to a holding label. Undo restores every message to its original folder, verified after a move affecting several folders. The holding label's retention is stated and enforced. Do not build yet: No rules or unsubscribe yet. ### Phase 4 · Rules, simulated before they run Build: Rules matching on sender, list, subject and age. Simulate against the existing mailbox and show what would have matched before enabling. Re-simulate when a rule is edited. Cap how much one rule may act on in a period. Trap: 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. Done when: Enabling a rule requires viewing its simulation against the existing mailbox. Editing a rule re-simulates before saving. A rule matching more than its cap in a period pauses and reports rather than continuing. Rule actions are recorded and undoable like manual ones. Do not build yet: No unsubscribe or reporting yet. ### Phase 5 · Unsubscribing without making it worse Build: Prefer the header-based unsubscribe mechanism where a message provides one. Treat a link in an unrecognised or suspicious message as unsafe and explain why rather than following it. Verify afterwards whether mail actually stopped. Trap: Clicking an unsubscribe link in unsolicited mail confirms to the sender that a real person opened it, which reliably produces more mail rather than less. Use the header mechanism, which is machine-to-machine and does not signal engagement, and where only a link exists, say what the trade is instead of deciding for the user. Done when: The header mechanism is used where present. A link-only unsubscribe in an unrecognised message is marked unsafe with an explanation rather than followed automatically. Mail arriving after an unsubscribe is detected and reported. Blocking a sender is offered as the alternative when unsubscribing is unsafe. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading or analysing message content, which would require a far broader permission than cleanup needs. - Provider coverage beyond one or two, which is most of the maintenance the hosted products carry. - Spam classification, which the provider already does better and with far more signal than you have. - Permanent deletion as a primary action. Everything here moves and retains, deliberately. ### README must contain - That nothing is deleted, only moved to a holding label with a stated retention - That every bulk action previews the exact count and a sample first - That undo restores each message to its original folder, because the origin is recorded per message - That rules are simulated against the existing mailbox before they can be enabled - That link-based unsubscribes in unrecognised mail are treated as unsafe and why ===== AGENTS.md ===== # Agent instructions · Clean Email indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Mail provider APIs with the narrowest scope that permits the operations, Clustering by sender, list identifier and subject pattern, Move to a holding label with retention, never delete, Simulated against the mailbox before enabling, Header mechanism preferred, links treated as unsafe by default. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Clean Email 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Seeing the mailbox without reading it You can understand a large mailbox from metadata alone. ### Steps 1. Request the narrowest working scope And document what it grants. Broad access is a liability even unused. Files: `src/oauth.ts` 2. Store metadata only Sender, subject, size, dates. Assert no bodies in the suite. Files: `src/sync.ts`, `tests/no-bodies.test.ts` 3. Sync incrementally and resumably Hundreds of thousands of messages. A restart from zero is unacceptable. Files: `src/sync.ts` ### Done when - [ ] A large mailbox syncs incrementally - [ ] No message body is stored - [ ] The narrowest scope is requested and documented - [ ] An interrupted sync resumes ### 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. ## Phase 2 · Grouping so cleanup is possible at all A hundred thousand messages become a few hundred decisions. ### Steps 1. Group by list identifier first It is the field designed for exactly this and it survives address rotation. Files: `src/group.ts` 2. Fall back to a normalised domain So one newsletter is one decision rather than fifty. Files: `src/group.ts` 3. Show count, range and size Size is what motivates cleanup. Range is what makes it safe. Files: `src/groups.ts` ### Done when - [ ] A rotating-address newsletter forms one group - [ ] Groups show count, date range and size - [ ] Subject patterns find recurring automated mail - [ ] A large mailbox yields a reviewable group count ### 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. ## Phase 3 · Bulk actions you can take back Every destructive action is previewed and reversible. ### Steps 1. Preview count and sample first The exact number, and messages the user can look at. Files: `src/preview.ts` 2. Move to holding, never delete Deletion is the risk. Retention makes it reversible for a while. Files: `src/actions.ts` 3. Record per-message origin So undo restores rather than piling everything in one folder. Files: `src/moved.ts`, `tests/undo.test.ts` ### Done when - [ ] Actions preview exact count and a sample - [ ] Nothing is deleted, only moved to holding - [ ] Undo restores every message to its original folder - [ ] Holding retention is stated and enforced ### 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. ## Phase 4 · Rules, simulated before they run A rule cannot surprise you after it is enabled. ### Steps 1. Simulate against history before enabling The count against real mail is the only useful preview. Files: `src/simulate.ts` 2. Re-simulate on edit An edited rule is a new rule. Files: `src/rules.ts` 3. Cap per-rule volume So a mistake is bounded rather than open ended. Files: `src/rules.ts` ### Done when - [ ] Enabling requires viewing a simulation - [ ] Editing re-simulates before saving - [ ] A rule exceeding its cap pauses and reports - [ ] Rule actions are undoable like manual ones ### 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. ## Not in this build - Reading or analysing message content. - Broad provider coverage. - Spam classification. - Permanent deletion as a primary action. ## After v1, if you want it - Attachment-focused cleanup, since size is usually what prompts this - A scheduled review of what rules did last month, which is where trust is maintained - Sender reputation from your own behaviour, so groups you always read are never suggested ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Message metadata. Senders and subjects reveal a great deal even without bodies. DATABASE_URL=postgres://localhost:5432/mailclean # Required. Present as a constant so the answer is visible. There is no other value. STORE_MESSAGE_BODIES=never # Required. Where bulk actions move mail. Nothing is deleted. HOLDING_LABEL=Cleanup Holding # Required. The window in which a mistake is recoverable. HOLDING_RETENTION_DAYS=60 # Required. How much one rule may act on before pausing. Bounds a mistake. RULE_VOLUME_CAP_PER_DAY=500 # Required. Clicking a link in unsolicited mail confirms a person is there. FOLLOW_UNSUBSCRIBE_LINKS=never_automatically
You are building a production product version of Clean Email. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Clean Email · product brief ## Problem The visible inbox cleanup loop is buildable, but a credible replacement needs more than the first screen. Clean Email earns its keep through deliverability, security, integration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A cleanup tool that can undo everything it did, previews every bulk action before running it, and will not tell a spammer you exist in order to unsubscribe you. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - IMAP or Gmail OAuth credentials where supported - secure local keychain access ## Explicit non-goals for v1 - Reading or analysing message content. - Broad provider coverage. - Spam classification. - Permanent deletion as a primary action. - sender reputation, deliverability, and suppression handling - provider-grade deliverability - push reliability across devices - team routing and shared inbox rules - abuse response and security operations ## Success criteria - No message body is confirmed stored - A rotating-address newsletter is confirmed to form one group - Undo is confirmed to restore original folders exactly - Enabling a rule is confirmed to require a simulation - Link-only unsubscribes are confirmed not followed automatically ===== BRIEF.md ===== # Build brief · Clean Email The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me bulk mailbox cleanup where every destructive action is reversible and previewed to replace Clean Email. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool deletes email in bulk, and email deletion is frequently irreversible. A rule that matches more than the user expected can remove years of correspondence in seconds, and the user will not find out for months. So: nothing is deleted. Everything moves to a holding label with a retention period, and actual deletion happens after that period or never. Every bulk action previews the exact count and a sample before running. Rules are simulated against the mailbox before they are enabled. And the tool records what it moved, so undoing is a real operation rather than a hope. The other trap is the unsubscribe button. Clicking an unsubscribe link in a message that was never legitimate confirms to the sender that a person read it, which produces more mail rather than less. Prefer the header-based unsubscribe mechanism where present, treat a link in an unrecognised message as unsafe, and say why rather than doing it quietly. ### Stack (fixed, do not substitute) - Access: Mail provider APIs with the narrowest scope that permits the operations · full mailbox access is a serious permission and this needs some of it - Grouping: Clustering by sender, list identifier and subject pattern · cleaning by individual message does not scale to a real mailbox - Safety: Move to a holding label with retention, never delete · the tool's own actions are the largest risk it carries - Rules: Simulated against the mailbox before enabling · a rule that matches too much is discovered months later - Unsubscribe: Header mechanism preferred, links treated as unsafe by default · clicking a link in spam confirms you exist ### Data model (create this before Phase 1) - 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 ### Phase 1 · Seeing the mailbox without reading it Build: Connect an account with the narrowest scope that permits the operations. Sync message metadata only, never bodies. Handle a mailbox with hundreds of thousands of messages without an unbounded initial sync. Trap: 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. Done when: Metadata for a large mailbox syncs incrementally without loading everything at once. No message body is stored anywhere, asserted in the test suite. The narrowest scope permitting the operations is requested and documented. An interrupted initial sync resumes rather than restarting. Do not build yet: No grouping, actions or rules. ### Phase 2 · Grouping so cleanup is possible at all Build: Cluster messages by sender, mailing list identifier and subject pattern. Show counts, date ranges and size per group. Handle a sender that uses many varying addresses for the same mail. Trap: 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. Done when: A newsletter using rotating sender addresses appears as one group, verified against a real mailbox. Groups show count, date range and total size. Subject-pattern grouping finds recurring automated mail that shares no sender. A hundred thousand messages produce a reviewable number of groups. Do not build yet: No actions, rules or unsubscribe yet. ### Phase 3 · Bulk actions you can take back Build: Preview the exact matched count and a sample before any bulk action. Move to a holding label with a retention period rather than deleting. Record exactly what moved from where, so undo restores each message to its original location. Trap: 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. Done when: A bulk action shows the exact count and a sample before executing. Nothing is deleted; messages move to a holding label. Undo restores every message to its original folder, verified after a move affecting several folders. The holding label's retention is stated and enforced. Do not build yet: No rules or unsubscribe yet. ### Phase 4 · Rules, simulated before they run Build: Rules matching on sender, list, subject and age. Simulate against the existing mailbox and show what would have matched before enabling. Re-simulate when a rule is edited. Cap how much one rule may act on in a period. Trap: 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. Done when: Enabling a rule requires viewing its simulation against the existing mailbox. Editing a rule re-simulates before saving. A rule matching more than its cap in a period pauses and reports rather than continuing. Rule actions are recorded and undoable like manual ones. Do not build yet: No unsubscribe or reporting yet. ### Phase 5 · Unsubscribing without making it worse Build: Prefer the header-based unsubscribe mechanism where a message provides one. Treat a link in an unrecognised or suspicious message as unsafe and explain why rather than following it. Verify afterwards whether mail actually stopped. Trap: Clicking an unsubscribe link in unsolicited mail confirms to the sender that a real person opened it, which reliably produces more mail rather than less. Use the header mechanism, which is machine-to-machine and does not signal engagement, and where only a link exists, say what the trade is instead of deciding for the user. Done when: The header mechanism is used where present. A link-only unsubscribe in an unrecognised message is marked unsafe with an explanation rather than followed automatically. Mail arriving after an unsubscribe is detected and reported. Blocking a sender is offered as the alternative when unsubscribing is unsafe. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading or analysing message content, which would require a far broader permission than cleanup needs. - Provider coverage beyond one or two, which is most of the maintenance the hosted products carry. - Spam classification, which the provider already does better and with far more signal than you have. - Permanent deletion as a primary action. Everything here moves and retains, deliberately. ### README must contain - That nothing is deleted, only moved to a holding label with a stated retention - That every bulk action previews the exact count and a sample first - That undo restores each message to its original folder, because the origin is recorded per message - That rules are simulated against the existing mailbox before they can be enabled - That link-based unsubscribes in unrecognised mail are treated as unsafe and why ===== ARCHITECTURE.md ===== # Architecture · Clean Email ## Stack | Part | Choice | Why | | --- | --- | --- | | Access | Mail provider APIs with the narrowest scope that permits the operations | full mailbox access is a serious permission and this needs some of it | | Grouping | Clustering by sender, list identifier and subject pattern | cleaning by individual message does not scale to a real mailbox | | Safety | Move to a holding label with retention, never delete | the tool's own actions are the largest risk it carries | | Rules | Simulated against the mailbox before enabling | a rule that matches too much is discovered months later | | Unsubscribe | Header mechanism preferred, links treated as unsafe by default | clicking a link in spam confirms you exist | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Sync | Metadata-only incremental sync. | Any provider, provided bodies are never stored and the scope is the narrowest that works. | | Grouping | Clustering by list, domain and pattern. | Any clustering, provided rotating sender addresses form one group. | | Actions | Preview, move and exact undo. | Not swappable for deletion, which is the risk the whole design exists to bound. | | Rules | Simulation and volume caps. | Any engine, provided enabling requires a simulation against real history. | | Unsubscribe | Header preference and link safety. | Any implementation, provided links in unrecognised mail are not followed automatically. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Message metadata. Senders and subjects reveal a great deal even without bodies. - `STORE_MESSAGE_BODIES` · required · Present as a constant so the answer is visible. There is no other value. - `HOLDING_LABEL` · required · Where bulk actions move mail. Nothing is deleted. - `HOLDING_RETENTION_DAYS` · required · The window in which a mistake is recoverable. - `RULE_VOLUME_CAP_PER_DAY` · required · How much one rule may act on before pausing. Bounds a mistake. - `FOLLOW_UNSUBSCRIBE_LINKS` · required · Clicking a link in unsolicited mail confirms a person is there. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Clean Email product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Mail provider APIs with the narrowest scope that permits the operations, Clustering by sender, list identifier and subject pattern, Move to a holding label with retention, never delete, Simulated against the mailbox before enabling, Header mechanism preferred, links treated as unsafe by default. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - Clicking an unsubscribe link in unsolicited mail confirms to the sender that a real person opened it, which reliably produces more mail rather than less. Use the header mechanism, which is machine-to-machine and does not signal engagement, and where only a link exists, say what the trade is instead of deciding for the user. ===== MILESTONES.md ===== # Delivery milestones · Clean Email Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Seeing the mailbox without reading it You can understand a large mailbox from metadata alone. ### Steps 1. Request the narrowest working scope And document what it grants. Broad access is a liability even unused. Files: `src/oauth.ts` 2. Store metadata only Sender, subject, size, dates. Assert no bodies in the suite. Files: `src/sync.ts`, `tests/no-bodies.test.ts` 3. Sync incrementally and resumably Hundreds of thousands of messages. A restart from zero is unacceptable. Files: `src/sync.ts` ### Done when - [ ] A large mailbox syncs incrementally - [ ] No message body is stored - [ ] The narrowest scope is requested and documented - [ ] An interrupted sync resumes ### 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. ## M2 · Grouping so cleanup is possible at all A hundred thousand messages become a few hundred decisions. ### Steps 1. Group by list identifier first It is the field designed for exactly this and it survives address rotation. Files: `src/group.ts` 2. Fall back to a normalised domain So one newsletter is one decision rather than fifty. Files: `src/group.ts` 3. Show count, range and size Size is what motivates cleanup. Range is what makes it safe. Files: `src/groups.ts` ### Done when - [ ] A rotating-address newsletter forms one group - [ ] Groups show count, date range and size - [ ] Subject patterns find recurring automated mail - [ ] A large mailbox yields a reviewable group count ### 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. ## M3 · Bulk actions you can take back Every destructive action is previewed and reversible. ### Steps 1. Preview count and sample first The exact number, and messages the user can look at. Files: `src/preview.ts` 2. Move to holding, never delete Deletion is the risk. Retention makes it reversible for a while. Files: `src/actions.ts` 3. Record per-message origin So undo restores rather than piling everything in one folder. Files: `src/moved.ts`, `tests/undo.test.ts` ### Done when - [ ] Actions preview exact count and a sample - [ ] Nothing is deleted, only moved to holding - [ ] Undo restores every message to its original folder - [ ] Holding retention is stated and enforced ### 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. ## M4 · Rules, simulated before they run A rule cannot surprise you after it is enabled. ### Steps 1. Simulate against history before enabling The count against real mail is the only useful preview. Files: `src/simulate.ts` 2. Re-simulate on edit An edited rule is a new rule. Files: `src/rules.ts` 3. Cap per-rule volume So a mistake is bounded rather than open ended. Files: `src/rules.ts` ### Done when - [ ] Enabling requires viewing a simulation - [ ] Editing re-simulates before saving - [ ] A rule exceeding its cap pauses and reports - [ ] Rule actions are undoable like manual ones ### 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. ## M5 · Unsubscribing without making it worse (production only) You stop receiving mail without confirming you exist to a spammer. ### Steps 1. Prefer the header mechanism Machine to machine. It does not tell the sender a person is there. Files: `src/unsubscribe.ts` 2. Mark link-only unsubscribes unsafe With an explanation. The user should decide, knowing the trade. Files: `src/unsubscribe.ts` 3. Verify whether it worked Mail arriving afterwards is the only real test. Files: `src/verify.ts` ### Done when - [ ] The header mechanism is used where present - [ ] Link-only unsubscribes are marked unsafe with a reason - [ ] Continued mail after unsubscribing is detected - [ ] Blocking is offered as the alternative ### Watch out - Clicking an unsubscribe link in unsolicited mail confirms to the sender that a real person opened it, which reliably produces more mail rather than less. Use the header mechanism, which is machine-to-machine and does not signal engagement, and where only a link exists, say what the trade is instead of deciding for the user. ===== OPERATIONS.md ===== # Operations · Clean Email ## Backup The mailbox itself is the provider's responsibility, but the move records are what make undo possible, so back them up and keep them longer than the holding retention. ## Restore Restore and confirm move records survived, since losing them turns a reversible operation into a permanent one after the fact, which is the exact outcome the design prevents. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Rule pause events and undo frequency. Frequent undos mean previews are not conveying what will happen, which is a design problem rather than a user error. ## Incident checklist Mail moved that should not have been is the failure, and it is recoverable only within the holding retention. That window is the control, which is why permanent deletion is not a primary action anywhere in the build. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No message body is confirmed stored - [ ] A rotating-address newsletter is confirmed to form one group - [ ] Undo is confirmed to restore original folders exactly - [ ] Enabling a rule is confirmed to require a simulation - [ ] Link-only unsubscribes are confirmed not followed automatically ## Launch constraint Do not market omitted Clean Email capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Message metadata. Senders and subjects reveal a great deal even without bodies. DATABASE_URL=postgres://localhost:5432/mailclean # Required. Present as a constant so the answer is visible. There is no other value. STORE_MESSAGE_BODIES=never # Required. Where bulk actions move mail. Nothing is deleted. HOLDING_LABEL=Cleanup Holding # Required. The window in which a mistake is recoverable. HOLDING_RETENTION_DAYS=60 # Required. How much one rule may act on before pausing. Bounds a mistake. RULE_VOLUME_CAP_PER_DAY=500 # Required. Clicking a link in unsolicited mail confirms a person is there. FOLLOW_UNSUBSCRIBE_LINKS=never_automatically
# Clean Email · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Access | Mail provider APIs with the narrowest scope that permits the operations | full mailbox access is a serious permission and this needs some of it | | Grouping | Clustering by sender, list identifier and subject pattern | cleaning by individual message does not scale to a real mailbox | | Safety | Move to a holding label with retention, never delete | the tool's own actions are the largest risk it carries | | Rules | Simulated against the mailbox before enabling | a rule that matches too much is discovered months later | | Unsubscribe | Header mechanism preferred, links treated as unsafe by default | clicking a link in spam confirms you exist | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the sync workers. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Message metadata at mailbox scale with grouping queries. - Get it: Docker or your package manager. - Verify: psql connects and groups a hundred thousand rows quickly - [ ] **A mail account with API access and a real history** · 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 - [ ] **A full backup of the mailbox you will test against** · free - 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 - [ ] **Your holding retention period** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Reading or analysing message content. - Broad provider coverage. - Spam classification. - Permanent deletion as a primary action. - sender reputation, deliverability, and suppression handling - provider-grade deliverability - push reliability across devices - team routing and shared inbox rules - abuse response and security operations If one of those is essential to you, that is the reason to keep paying for Clean Email, and the README should say so rather than pretend.
# Build brief · Clean Email The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me bulk mailbox cleanup where every destructive action is reversible and previewed to replace Clean Email. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool deletes email in bulk, and email deletion is frequently irreversible. A rule that matches more than the user expected can remove years of correspondence in seconds, and the user will not find out for months. So: nothing is deleted. Everything moves to a holding label with a retention period, and actual deletion happens after that period or never. Every bulk action previews the exact count and a sample before running. Rules are simulated against the mailbox before they are enabled. And the tool records what it moved, so undoing is a real operation rather than a hope. The other trap is the unsubscribe button. Clicking an unsubscribe link in a message that was never legitimate confirms to the sender that a person read it, which produces more mail rather than less. Prefer the header-based unsubscribe mechanism where present, treat a link in an unrecognised message as unsafe, and say why rather than doing it quietly. ### Stack (fixed, do not substitute) - Access: Mail provider APIs with the narrowest scope that permits the operations · full mailbox access is a serious permission and this needs some of it - Grouping: Clustering by sender, list identifier and subject pattern · cleaning by individual message does not scale to a real mailbox - Safety: Move to a holding label with retention, never delete · the tool's own actions are the largest risk it carries - Rules: Simulated against the mailbox before enabling · a rule that matches too much is discovered months later - Unsubscribe: Header mechanism preferred, links treated as unsafe by default · clicking a link in spam confirms you exist ### Data model (create this before Phase 1) - 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 ### Phase 1 · Seeing the mailbox without reading it Build: Connect an account with the narrowest scope that permits the operations. Sync message metadata only, never bodies. Handle a mailbox with hundreds of thousands of messages without an unbounded initial sync. Trap: 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. Done when: Metadata for a large mailbox syncs incrementally without loading everything at once. No message body is stored anywhere, asserted in the test suite. The narrowest scope permitting the operations is requested and documented. An interrupted initial sync resumes rather than restarting. Do not build yet: No grouping, actions or rules. ### Phase 2 · Grouping so cleanup is possible at all Build: Cluster messages by sender, mailing list identifier and subject pattern. Show counts, date ranges and size per group. Handle a sender that uses many varying addresses for the same mail. Trap: 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. Done when: A newsletter using rotating sender addresses appears as one group, verified against a real mailbox. Groups show count, date range and total size. Subject-pattern grouping finds recurring automated mail that shares no sender. A hundred thousand messages produce a reviewable number of groups. Do not build yet: No actions, rules or unsubscribe yet. ### Phase 3 · Bulk actions you can take back Build: Preview the exact matched count and a sample before any bulk action. Move to a holding label with a retention period rather than deleting. Record exactly what moved from where, so undo restores each message to its original location. Trap: 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. Done when: A bulk action shows the exact count and a sample before executing. Nothing is deleted; messages move to a holding label. Undo restores every message to its original folder, verified after a move affecting several folders. The holding label's retention is stated and enforced. Do not build yet: No rules or unsubscribe yet. ### Phase 4 · Rules, simulated before they run Build: Rules matching on sender, list, subject and age. Simulate against the existing mailbox and show what would have matched before enabling. Re-simulate when a rule is edited. Cap how much one rule may act on in a period. Trap: 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. Done when: Enabling a rule requires viewing its simulation against the existing mailbox. Editing a rule re-simulates before saving. A rule matching more than its cap in a period pauses and reports rather than continuing. Rule actions are recorded and undoable like manual ones. Do not build yet: No unsubscribe or reporting yet. ### Phase 5 · Unsubscribing without making it worse Build: Prefer the header-based unsubscribe mechanism where a message provides one. Treat a link in an unrecognised or suspicious message as unsafe and explain why rather than following it. Verify afterwards whether mail actually stopped. Trap: Clicking an unsubscribe link in unsolicited mail confirms to the sender that a real person opened it, which reliably produces more mail rather than less. Use the header mechanism, which is machine-to-machine and does not signal engagement, and where only a link exists, say what the trade is instead of deciding for the user. Done when: The header mechanism is used where present. A link-only unsubscribe in an unrecognised message is marked unsafe with an explanation rather than followed automatically. Mail arriving after an unsubscribe is detected and reported. Blocking a sender is offered as the alternative when unsubscribing is unsafe. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading or analysing message content, which would require a far broader permission than cleanup needs. - Provider coverage beyond one or two, which is most of the maintenance the hosted products carry. - Spam classification, which the provider already does better and with far more signal than you have. - Permanent deletion as a primary action. Everything here moves and retains, deliberately. ### README must contain - That nothing is deleted, only moved to a holding label with a stated retention - That every bulk action previews the exact count and a sample first - That undo restores each message to its original folder, because the origin is recorded per message - That rules are simulated against the existing mailbox before they can be enabled - That link-based unsubscribes in unrecognised mail are treated as unsafe and why
# Agent instructions · Clean Email indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Mail provider APIs with the narrowest scope that permits the operations, Clustering by sender, list identifier and subject pattern, Move to a holding label with retention, never delete, Simulated against the mailbox before enabling, Header mechanism preferred, links treated as unsafe by default. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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.
# Build plan · Clean Email 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Seeing the mailbox without reading it You can understand a large mailbox from metadata alone. ### Steps 1. Request the narrowest working scope And document what it grants. Broad access is a liability even unused. Files: `src/oauth.ts` 2. Store metadata only Sender, subject, size, dates. Assert no bodies in the suite. Files: `src/sync.ts`, `tests/no-bodies.test.ts` 3. Sync incrementally and resumably Hundreds of thousands of messages. A restart from zero is unacceptable. Files: `src/sync.ts` ### Done when - [ ] A large mailbox syncs incrementally - [ ] No message body is stored - [ ] The narrowest scope is requested and documented - [ ] An interrupted sync resumes ### 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. ## Phase 2 · Grouping so cleanup is possible at all A hundred thousand messages become a few hundred decisions. ### Steps 1. Group by list identifier first It is the field designed for exactly this and it survives address rotation. Files: `src/group.ts` 2. Fall back to a normalised domain So one newsletter is one decision rather than fifty. Files: `src/group.ts` 3. Show count, range and size Size is what motivates cleanup. Range is what makes it safe. Files: `src/groups.ts` ### Done when - [ ] A rotating-address newsletter forms one group - [ ] Groups show count, date range and size - [ ] Subject patterns find recurring automated mail - [ ] A large mailbox yields a reviewable group count ### 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. ## Phase 3 · Bulk actions you can take back Every destructive action is previewed and reversible. ### Steps 1. Preview count and sample first The exact number, and messages the user can look at. Files: `src/preview.ts` 2. Move to holding, never delete Deletion is the risk. Retention makes it reversible for a while. Files: `src/actions.ts` 3. Record per-message origin So undo restores rather than piling everything in one folder. Files: `src/moved.ts`, `tests/undo.test.ts` ### Done when - [ ] Actions preview exact count and a sample - [ ] Nothing is deleted, only moved to holding - [ ] Undo restores every message to its original folder - [ ] Holding retention is stated and enforced ### 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. ## Phase 4 · Rules, simulated before they run A rule cannot surprise you after it is enabled. ### Steps 1. Simulate against history before enabling The count against real mail is the only useful preview. Files: `src/simulate.ts` 2. Re-simulate on edit An edited rule is a new rule. Files: `src/rules.ts` 3. Cap per-rule volume So a mistake is bounded rather than open ended. Files: `src/rules.ts` ### Done when - [ ] Enabling requires viewing a simulation - [ ] Editing re-simulates before saving - [ ] A rule exceeding its cap pauses and reports - [ ] Rule actions are undoable like manual ones ### 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. ## Not in this build - Reading or analysing message content. - Broad provider coverage. - Spam classification. - Permanent deletion as a primary action. ## After v1, if you want it - Attachment-focused cleanup, since size is usually what prompts this - A scheduled review of what rules did last month, which is where trust is maintained - Sender reputation from your own behaviour, so groups you always read are never suggested
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Message metadata. Senders and subjects reveal a great deal even without bodies. DATABASE_URL=postgres://localhost:5432/mailclean # Required. Present as a constant so the answer is visible. There is no other value. STORE_MESSAGE_BODIES=never # Required. Where bulk actions move mail. Nothing is deleted. HOLDING_LABEL=Cleanup Holding # Required. The window in which a mistake is recoverable. HOLDING_RETENTION_DAYS=60 # Required. How much one rule may act on before pausing. Bounds a mistake. RULE_VOLUME_CAP_PER_DAY=500 # Required. Clicking a link in unsolicited mail confirms a person is there. FOLLOW_UNSUBSCRIBE_LINKS=never_automatically
# Clean Email · product brief ## Problem The visible inbox cleanup loop is buildable, but a credible replacement needs more than the first screen. Clean Email earns its keep through deliverability, security, integration, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A cleanup tool that can undo everything it did, previews every bulk action before running it, and will not tell a spammer you exist in order to unsubscribe you. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - IMAP or Gmail OAuth credentials where supported - secure local keychain access ## Explicit non-goals for v1 - Reading or analysing message content. - Broad provider coverage. - Spam classification. - Permanent deletion as a primary action. - sender reputation, deliverability, and suppression handling - provider-grade deliverability - push reliability across devices - team routing and shared inbox rules - abuse response and security operations ## Success criteria - No message body is confirmed stored - A rotating-address newsletter is confirmed to form one group - Undo is confirmed to restore original folders exactly - Enabling a rule is confirmed to require a simulation - Link-only unsubscribes are confirmed not followed automatically
# Build brief · Clean Email The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me bulk mailbox cleanup where every destructive action is reversible and previewed to replace Clean Email. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool deletes email in bulk, and email deletion is frequently irreversible. A rule that matches more than the user expected can remove years of correspondence in seconds, and the user will not find out for months. So: nothing is deleted. Everything moves to a holding label with a retention period, and actual deletion happens after that period or never. Every bulk action previews the exact count and a sample before running. Rules are simulated against the mailbox before they are enabled. And the tool records what it moved, so undoing is a real operation rather than a hope. The other trap is the unsubscribe button. Clicking an unsubscribe link in a message that was never legitimate confirms to the sender that a person read it, which produces more mail rather than less. Prefer the header-based unsubscribe mechanism where present, treat a link in an unrecognised message as unsafe, and say why rather than doing it quietly. ### Stack (fixed, do not substitute) - Access: Mail provider APIs with the narrowest scope that permits the operations · full mailbox access is a serious permission and this needs some of it - Grouping: Clustering by sender, list identifier and subject pattern · cleaning by individual message does not scale to a real mailbox - Safety: Move to a holding label with retention, never delete · the tool's own actions are the largest risk it carries - Rules: Simulated against the mailbox before enabling · a rule that matches too much is discovered months later - Unsubscribe: Header mechanism preferred, links treated as unsafe by default · clicking a link in spam confirms you exist ### Data model (create this before Phase 1) - 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 ### Phase 1 · Seeing the mailbox without reading it Build: Connect an account with the narrowest scope that permits the operations. Sync message metadata only, never bodies. Handle a mailbox with hundreds of thousands of messages without an unbounded initial sync. Trap: 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. Done when: Metadata for a large mailbox syncs incrementally without loading everything at once. No message body is stored anywhere, asserted in the test suite. The narrowest scope permitting the operations is requested and documented. An interrupted initial sync resumes rather than restarting. Do not build yet: No grouping, actions or rules. ### Phase 2 · Grouping so cleanup is possible at all Build: Cluster messages by sender, mailing list identifier and subject pattern. Show counts, date ranges and size per group. Handle a sender that uses many varying addresses for the same mail. Trap: 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. Done when: A newsletter using rotating sender addresses appears as one group, verified against a real mailbox. Groups show count, date range and total size. Subject-pattern grouping finds recurring automated mail that shares no sender. A hundred thousand messages produce a reviewable number of groups. Do not build yet: No actions, rules or unsubscribe yet. ### Phase 3 · Bulk actions you can take back Build: Preview the exact matched count and a sample before any bulk action. Move to a holding label with a retention period rather than deleting. Record exactly what moved from where, so undo restores each message to its original location. Trap: 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. Done when: A bulk action shows the exact count and a sample before executing. Nothing is deleted; messages move to a holding label. Undo restores every message to its original folder, verified after a move affecting several folders. The holding label's retention is stated and enforced. Do not build yet: No rules or unsubscribe yet. ### Phase 4 · Rules, simulated before they run Build: Rules matching on sender, list, subject and age. Simulate against the existing mailbox and show what would have matched before enabling. Re-simulate when a rule is edited. Cap how much one rule may act on in a period. Trap: 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. Done when: Enabling a rule requires viewing its simulation against the existing mailbox. Editing a rule re-simulates before saving. A rule matching more than its cap in a period pauses and reports rather than continuing. Rule actions are recorded and undoable like manual ones. Do not build yet: No unsubscribe or reporting yet. ### Phase 5 · Unsubscribing without making it worse Build: Prefer the header-based unsubscribe mechanism where a message provides one. Treat a link in an unrecognised or suspicious message as unsafe and explain why rather than following it. Verify afterwards whether mail actually stopped. Trap: Clicking an unsubscribe link in unsolicited mail confirms to the sender that a real person opened it, which reliably produces more mail rather than less. Use the header mechanism, which is machine-to-machine and does not signal engagement, and where only a link exists, say what the trade is instead of deciding for the user. Done when: The header mechanism is used where present. A link-only unsubscribe in an unrecognised message is marked unsafe with an explanation rather than followed automatically. Mail arriving after an unsubscribe is detected and reported. Blocking a sender is offered as the alternative when unsubscribing is unsafe. Do not build yet: Nothing after this. ### Out of scope (and why) - Reading or analysing message content, which would require a far broader permission than cleanup needs. - Provider coverage beyond one or two, which is most of the maintenance the hosted products carry. - Spam classification, which the provider already does better and with far more signal than you have. - Permanent deletion as a primary action. Everything here moves and retains, deliberately. ### README must contain - That nothing is deleted, only moved to a holding label with a stated retention - That every bulk action previews the exact count and a sample first - That undo restores each message to its original folder, because the origin is recorded per message - That rules are simulated against the existing mailbox before they can be enabled - That link-based unsubscribes in unrecognised mail are treated as unsafe and why
# Architecture · Clean Email ## Stack | Part | Choice | Why | | --- | --- | --- | | Access | Mail provider APIs with the narrowest scope that permits the operations | full mailbox access is a serious permission and this needs some of it | | Grouping | Clustering by sender, list identifier and subject pattern | cleaning by individual message does not scale to a real mailbox | | Safety | Move to a holding label with retention, never delete | the tool's own actions are the largest risk it carries | | Rules | Simulated against the mailbox before enabling | a rule that matches too much is discovered months later | | Unsubscribe | Header mechanism preferred, links treated as unsafe by default | clicking a link in spam confirms you exist | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Sync | Metadata-only incremental sync. | Any provider, provided bodies are never stored and the scope is the narrowest that works. | | Grouping | Clustering by list, domain and pattern. | Any clustering, provided rotating sender addresses form one group. | | Actions | Preview, move and exact undo. | Not swappable for deletion, which is the risk the whole design exists to bound. | | Rules | Simulation and volume caps. | Any engine, provided enabling requires a simulation against real history. | | Unsubscribe | Header preference and link safety. | Any implementation, provided links in unrecognised mail are not followed automatically. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Message metadata. Senders and subjects reveal a great deal even without bodies. - `STORE_MESSAGE_BODIES` · required · Present as a constant so the answer is visible. There is no other value. - `HOLDING_LABEL` · required · Where bulk actions move mail. Nothing is deleted. - `HOLDING_RETENTION_DAYS` · required · The window in which a mistake is recoverable. - `RULE_VOLUME_CAP_PER_DAY` · required · How much one rule may act on before pausing. Bounds a mistake. - `FOLLOW_UNSUBSCRIBE_LINKS` · required · Clicking a link in unsolicited mail confirms a person is there. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Clean Email product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Mail provider APIs with the narrowest scope that permits the operations, Clustering by sender, list identifier and subject pattern, Move to a holding label with retention, never delete, Simulated against the mailbox before enabling, Header mechanism preferred, links treated as unsafe by default. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - Clicking an unsubscribe link in unsolicited mail confirms to the sender that a real person opened it, which reliably produces more mail rather than less. Use the header mechanism, which is machine-to-machine and does not signal engagement, and where only a link exists, say what the trade is instead of deciding for the user.
# Delivery milestones · Clean Email Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Seeing the mailbox without reading it You can understand a large mailbox from metadata alone. ### Steps 1. Request the narrowest working scope And document what it grants. Broad access is a liability even unused. Files: `src/oauth.ts` 2. Store metadata only Sender, subject, size, dates. Assert no bodies in the suite. Files: `src/sync.ts`, `tests/no-bodies.test.ts` 3. Sync incrementally and resumably Hundreds of thousands of messages. A restart from zero is unacceptable. Files: `src/sync.ts` ### Done when - [ ] A large mailbox syncs incrementally - [ ] No message body is stored - [ ] The narrowest scope is requested and documented - [ ] An interrupted sync resumes ### 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. ## M2 · Grouping so cleanup is possible at all A hundred thousand messages become a few hundred decisions. ### Steps 1. Group by list identifier first It is the field designed for exactly this and it survives address rotation. Files: `src/group.ts` 2. Fall back to a normalised domain So one newsletter is one decision rather than fifty. Files: `src/group.ts` 3. Show count, range and size Size is what motivates cleanup. Range is what makes it safe. Files: `src/groups.ts` ### Done when - [ ] A rotating-address newsletter forms one group - [ ] Groups show count, date range and size - [ ] Subject patterns find recurring automated mail - [ ] A large mailbox yields a reviewable group count ### 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. ## M3 · Bulk actions you can take back Every destructive action is previewed and reversible. ### Steps 1. Preview count and sample first The exact number, and messages the user can look at. Files: `src/preview.ts` 2. Move to holding, never delete Deletion is the risk. Retention makes it reversible for a while. Files: `src/actions.ts` 3. Record per-message origin So undo restores rather than piling everything in one folder. Files: `src/moved.ts`, `tests/undo.test.ts` ### Done when - [ ] Actions preview exact count and a sample - [ ] Nothing is deleted, only moved to holding - [ ] Undo restores every message to its original folder - [ ] Holding retention is stated and enforced ### 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. ## M4 · Rules, simulated before they run A rule cannot surprise you after it is enabled. ### Steps 1. Simulate against history before enabling The count against real mail is the only useful preview. Files: `src/simulate.ts` 2. Re-simulate on edit An edited rule is a new rule. Files: `src/rules.ts` 3. Cap per-rule volume So a mistake is bounded rather than open ended. Files: `src/rules.ts` ### Done when - [ ] Enabling requires viewing a simulation - [ ] Editing re-simulates before saving - [ ] A rule exceeding its cap pauses and reports - [ ] Rule actions are undoable like manual ones ### 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. ## M5 · Unsubscribing without making it worse (production only) You stop receiving mail without confirming you exist to a spammer. ### Steps 1. Prefer the header mechanism Machine to machine. It does not tell the sender a person is there. Files: `src/unsubscribe.ts` 2. Mark link-only unsubscribes unsafe With an explanation. The user should decide, knowing the trade. Files: `src/unsubscribe.ts` 3. Verify whether it worked Mail arriving afterwards is the only real test. Files: `src/verify.ts` ### Done when - [ ] The header mechanism is used where present - [ ] Link-only unsubscribes are marked unsafe with a reason - [ ] Continued mail after unsubscribing is detected - [ ] Blocking is offered as the alternative ### Watch out - Clicking an unsubscribe link in unsolicited mail confirms to the sender that a real person opened it, which reliably produces more mail rather than less. Use the header mechanism, which is machine-to-machine and does not signal engagement, and where only a link exists, say what the trade is instead of deciding for the user.
# Operations · Clean Email ## Backup The mailbox itself is the provider's responsibility, but the move records are what make undo possible, so back them up and keep them longer than the holding retention. ## Restore Restore and confirm move records survived, since losing them turns a reversible operation into a permanent one after the fact, which is the exact outcome the design prevents. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Rule pause events and undo frequency. Frequent undos mean previews are not conveying what will happen, which is a design problem rather than a user error. ## Incident checklist Mail moved that should not have been is the failure, and it is recoverable only within the holding retention. That window is the control, which is why permanent deletion is not a primary action anywhere in the build. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No message body is confirmed stored - [ ] A rotating-address newsletter is confirmed to form one group - [ ] Undo is confirmed to restore original folders exactly - [ ] Enabling a rule is confirmed to require a simulation - [ ] Link-only unsubscribes are confirmed not followed automatically ## Launch constraint Do not market omitted Clean Email capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Message metadata. Senders and subjects reveal a great deal even without bodies. DATABASE_URL=postgres://localhost:5432/mailclean # Required. Present as a constant so the answer is visible. There is no other value. STORE_MESSAGE_BODIES=never # Required. Where bulk actions move mail. Nothing is deleted. HOLDING_LABEL=Cleanup Holding # Required. The window in which a mistake is recoverable. HOLDING_RETENTION_DAYS=60 # Required. How much one rule may act on before pausing. Bounds a mistake. RULE_VOLUME_CAP_PER_DAY=500 # Required. Clicking a link in unsolicited mail confirms a person is there. FOLLOW_UNSUBSCRIBE_LINKS=never_automatically
$ choose a build depth, inspect the files, then open the complete pack in your agent
Clean Email: Email is infrastructure. Customers pay to avoid lost messages, broken auth, spam problems, and the reputational cost of a flaky inbox.
xsender reputation, deliverability, and suppression handling
xprovider-grade deliverability
xpush reliability across devices
xteam routing and shared inbox rules
xabuse response and security operations
Don't feel like building it? These folks already made it free.
all 3 free alternatives to Clean Email →· no votes, no pay-to-list · just what's real
Clean Email pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | Clean up to 1,000 messages; unsubscribe from 25 subscriptions; no Auto Clean, Screener, or premium sender/list settings |
| premium, 1 account | $9.99 | $2.50 | 1 email account; unlimited email cleaning; all current and future features |
| premium, 5 accounts | $19.99 | $4.17 | 5 email accounts; unlimited email cleaning; all current and future features |
| premium, 10 accounts | $29.99 | $8.33 | 10 email accounts; unlimited email cleaning; all current and future features |
free tierClean up to 1,000 messages and unsubscribe from 25 subscriptions; premium-only automation/settings excluded; optional 14-day premium trial
billingmonthly + annual; annual plans save up to 70%; cancel or change plan at any time
hidden costsPricing jumps by mailbox bundle: managing more than 1 account requires the 5-account plan, and more than 5 requires the 10-account plan.
verified 2026-08-12 · source ↗
Vibecode Clean Email
Kinda. The core of Clean Email is buildable in a weekend with the prompt on this page, but there are real gaps: sender reputation, deliverability, and suppression handling, provider-grade deliverability. Read the honest list above before committing.
How much does Clean Email cost?
Clean Email's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Clean Email?
Honestly: sender reputation, deliverability, and suppression handling; provider-grade deliverability; push reliability across devices; team routing and shared inbox rules; abuse response and security operations. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Clean Email?
Yes: Edison Mail (Bulk delete, unsubscribe, rules and sender blocking for free; opt out of the shopping-data research if that sentence bothered you.) Gmailytics (Gmail cleanup by sender with unlimited unsubscribes; background automation and other mail providers stayed home.) Inbox Zero (Rules, bulk unsubscribe, cold-email blocking and cleanup under your own roof; OAuth and Docker are the cover charge.) All 3 curated free alternatives are at vibecodeit.com/clean-email/alternatives. The prompt is for when you want it exactly your way.