Build Mailbutler
KINDAback to the verdict
A local companion for one mailbox whose hard problems are keeping annotations attached to the right message when a server changes its identifiers, cancelling a follow-up only when a real reply arrives, and reporting tracking honestly enough to be worth having.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why A desktop process that holds a mailbox credential and a local database, rather than a server that holds your mail.
Get it rustup.rs, then the Tauri 2 prerequisites page for your platform. open ↗
Verify
cargo --version prints and a blank Tauri 2 application launches - installfree
Why Builds the interface and runs its tests.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - accountfree with most providers
Why This build touches exactly one mailbox, and using an app password rather than your account password means the credential can be revoked on its own.
Get it Create an app password in your provider's security settings, or register an OAuth client with mail scopes.
Verify
An IMAP session lists folders and an SMTP session authenticates - have readyfree
Why Forcing a UIDVALIDITY change is the only way to test the remap, and you cannot do it on a real mailbox.
Get it Any small IMAP server you can run locally and reset, seeded with a few thousand messages.
Verify
UIDVALIDITY can be changed on demand and the client notices - have readyfree
Why Threading, auto-replies and forwarded chains only go wrong on real mail, and a generated corpus has none of those shapes.
Get it Export your own mail, or assemble a few thousand messages including auto-responders and a thread with a reused subject.
Verify
The corpus contains at least one auto-reply and one unrelated message sharing a subject - decidefree
Why All three are asserted in tests, and the tracking stance in particular is a promise the interface has to keep.
Get it Write down how late a scheduled send may still go, how long until the application locks, and exactly what wording the tracking panel uses.
Verify
All three appear in configuration and are named in tests
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- accounts(id, host, port, username, secret_ref, uidvalidity, last_seen_uid, last_sync_at, state) · secret_ref names a keychain entry and never holds a value - messages(message_id, account_id, folder, uid, uidvalidity, subject, from_addr, to_json, date, snippet, thread_key) · an index, with bodies fetched on demand - notes(message_id, body_md, created_at, updated_at) · yours, local, and never written into the message - tasks(id, message_id, title, due_at, done_at) - followups(id, thread_key, message_id, due_at, cancel_on_reply, state, cancelled_by_message_id) · the cancelling message is recorded, so a wrong cancellation is explainable - scheduled_sends(id, mime_path, to_json, send_at, state, attempts, sent_message_id) · the composed message sits on disk, so a restart loses nothing - remote_fetches(message_id, at, user_agent, network_class, verdict) · verdict is fetched, and no value in this column means read - orphans(annotation_kind, annotation_id, lost_message_id, at) · annotations whose message could not be remapped, shown rather than reattached by guesswork
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATA_DIR | required | ~/.local/share/mailcompanion | The local database and the composed message spool. Removed entirely by the delete command. |
IMAP_HOST | required | imap.example.com | One mailbox, over TLS. |
IMAP_USERNAME | required | you@example.com | The account. Its secret is in the keychain, not here. |
KEYCHAIN_SERVICE | required | mailcompanion | Names the keychain entry holding the mailbox secret. |
SMTP_HOST | required | smtp.example.com | Used only when you send something you composed. |
MISSED_WINDOW_MINUTES | required | 30 | A scheduled send later than this is held and asked about rather than sent. |
IDLE_LOCK_MINUTES | required | 15 | Locks the application. An unlocked mailbox on a shared desk is the exposure. |
TRACKING_ENABLED | required | false | False after installation. Enabling it adds a disclosure line to every message. |
The build, in order
Reading a mailbox without breaking it
The index survives a server changing its identifiers, and nothing is ever deleted.
It is the only identifier that survives the server's housekeeping.
Files
src/mail/identity.tstests/uidvalidity.test.tsThe strongest guarantee is the one the protocol enforces.
Files
src/mail/imap.tsA note on the wrong email is worse than a note with no email.
Files
src/ui/Orphans.tsx
done when · tick each as it passeswatch out- Caching messages by UID and resuming after the server has changed UIDVALIDITY points every stored annotation at a different message, and on a large mailbox nobody notices until a private note about a difficult client appears attached to a stranger's email. Key annotations on Message-Id, treat a UIDVALIDITY change as a remap of the whole folder, and show an annotation whose remap failed as orphaned rather than attaching it to a best guess.
Notes and tasks that live beside the mail
Annotations are local, exportable, and invisible to every other client.
The mailbox stays exactly as the other clients left it.
Files
src/store/notes.tsThe test that proves the first rule rather than asserting it.
Files
tests/untouched.test.tsAnnotations without their message links are a pile of text.
Files
src/export/annotations.ts
done when · tick each as it passeswatch out- Storing a note by appending to the message body, or by uploading an edited copy back to the folder, rewrites the message, changes its identifier, invalidates its signature and leaves two copies in the mailbox, and the person discovers it when their phone shows the same email twice. Keep annotations in the local database and never upload a modified copy of an existing message.
Follow-ups that cancel themselves
A follow-up disappears the moment the person actually replies, and not before.
Subjects are a last resort, not a strategy.
Files
src/mail/threading.tstests/threading.test.tsSo a wrong cancellation can be explained rather than argued about.
Files
src/followups/cancel.tsA holiday responder is not a reply.
Files
src/mail/autoreply.ts
done when · tick each as it passeswatch out- Matching a thread on the subject line alone means an unrelated message headed 'Re: invoice' from a different person quietly cancels your follow-up, and a holiday auto-responder cancels it just as effectively, so the follow-up you were relying on simply never appears. Thread on References and In-Reply-To, fall back to the subject only when both are missing, and skip messages that declare themselves automatic.
Scheduled send that sends once, or not at all
A message scheduled for the morning either goes at the right time or explains why it did not.
The lock happens before the network, or it does not help.
Files
src/schedule/send.tstests/once.test.tsA restart should cost nothing but a moment.
Files
src/compose/spool.tsHeld with a reason beats sent at a strange hour.
Files
src/schedule/policy.ts
done when · tick each as it passeswatch out- A laptop asleep at the send time either wakes at eleven and sends a good-morning message at an hour that reads oddly, or never sends it and nobody notices until the recipient does not reply. Decide the missed window explicitly, show held messages on the list with the reason, and test the behaviour by suspending the machine across a scheduled time.
Telling the truth about tracking, and holding the credential properlyproduct builder
The tracking feature is honest and off by default, and the mailbox secret lives in the keychain.
Both are asserted in the suite, not left to intention.
Files
src/tracking/config.tstests/disclosure.test.tsA search in the test suite is a cheap way to keep a promise.
Files
tests/no-read-claim.test.tsAnd prove by search that it reached no file and no log.
Files
src/secrets/keychain.ts
done when · tick each as it passeswatch out- Recording a pixel fetch as a read and deriving a city from the connecting address produces confident statements that are frequently false, because privacy proxies fetch every image on receipt and relay it from somewhere else entirely, and acting on those statements damages real relationships with people who did nothing. Report a fetch as a fetch, drop location entirely, and put the caveat next to the number rather than in the documentation.
That is the whole plan for Mailbutler. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Shared inboxes and team assignment.
- Two-way CRM synchronisation.
- A hosted tracking service.
- Mail merge at volume.
- team routing and shared inbox rules
- abuse response and security operations
- calendar-provider edge cases and timezone correctness
- provider-grade deliverability
- push reliability across devices
- Snoozing a message locally, which needs nothing the follow-up machinery does not already have
- Link click recording with the same honest wording, once the tracking panel has proved it refuses to overclaim
- A second mailbox, which mostly means proving the identifier mapping holds per account
Need the files? The project pack on the verdict page hands your agent the whole brief · more email.