Build Mailbutler

KINDAback to the verdict

0%0 of 18 items done

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

A 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.

estimated effort weekendthe files for this build are in the project pack

AccessIMAP and SMTP against one mailbox, with an app password or OAuthIdentityMessage-Id as the durable key, with folder, UID and UIDVALIDITY as a mappingStorageSQLite holding your annotations and an index of the mailbox, not a copy of itScheduled sendA local scheduler holding the composed message on disk until its timeSecretsThe platform keychain, never the database and never a file in the repositoryTrackingOff by default, reported as a remote content fetch and never as a read

Before step 1

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

  1. installfree

    Why 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

  2. 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

  3. free 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

  4. 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

  5. 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

  6. 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.

VariableNeededExampleWhere the value comes from
DATA_DIRrequired~/.local/share/mailcompanionThe local database and the composed message spool. Removed entirely by the delete command.
IMAP_HOSTrequiredimap.example.comOne mailbox, over TLS.
IMAP_USERNAMErequiredyou@example.comThe account. Its secret is in the keychain, not here.
KEYCHAIN_SERVICErequiredmailcompanionNames the keychain entry holding the mailbox secret.
SMTP_HOSTrequiredsmtp.example.comUsed only when you send something you composed.
MISSED_WINDOW_MINUTESrequired30A scheduled send later than this is held and asked about rather than sent.
IDLE_LOCK_MINUTESrequired15Locks the application. An unlocked mailbox on a shared desk is the exposure.
TRACKING_ENABLEDrequiredfalseFalse after installation. Enabling it adds a disclosure line to every message.

The build, in order

  1. Reading a mailbox without breaking it

    The index survives a server changing its identifiers, and nothing is ever deleted.

    1. It is the only identifier that survives the server's housekeeping.

      Files src/mail/identity.tstests/uidvalidity.test.ts

    2. The strongest guarantee is the one the protocol enforces.

      Files src/mail/imap.ts

    3. A note on the wrong email is worse than a note with no email.

      Files src/ui/Orphans.tsx

    done when · tick each as it passes
    watch 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.
  2. Notes and tasks that live beside the mail

    Annotations are local, exportable, and invisible to every other client.

    1. The mailbox stays exactly as the other clients left it.

      Files src/store/notes.ts

    2. The test that proves the first rule rather than asserting it.

      Files tests/untouched.test.ts

    3. Annotations without their message links are a pile of text.

      Files src/export/annotations.ts

    done when · tick each as it passes
    watch 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.
  3. Follow-ups that cancel themselves

    A follow-up disappears the moment the person actually replies, and not before.

    1. Subjects are a last resort, not a strategy.

      Files src/mail/threading.tstests/threading.test.ts

    2. So a wrong cancellation can be explained rather than argued about.

      Files src/followups/cancel.ts

    3. A holiday responder is not a reply.

      Files src/mail/autoreply.ts

    done when · tick each as it passes
    watch 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.
  4. 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.

    1. The lock happens before the network, or it does not help.

      Files src/schedule/send.tstests/once.test.ts

    2. A restart should cost nothing but a moment.

      Files src/compose/spool.ts

    3. Held with a reason beats sent at a strange hour.

      Files src/schedule/policy.ts

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

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