The webmail app
A plain-browser webmail client: the folder rail, a newest-first message
list with per-folder search, a sandboxed reader, and a compose pane —
plus the same wallet login and settings panes the Thunderbird and
Outlook clients carry, because all three hosts run the same shared core
(webclients/shared/). Everything that must be signed is signed
in the page by a WebAssembly module compiled from this workspace’s
own crates; the server only relays already-signed transactions and
never holds your key.
Navigation is hash-only — #/mail (the three-pane mail view) and
#/settings (the shared dashboard panes). There is no build framework
and no CDN: the bundle is static files served by
account-api itself.
The server is optional, though: clearing the API URL in the connection settings switches the app into trustless mode — wallet-only unlock, on-chain mail read and sent with nothing but a Solana RPC node and an IPFS gateway/pin daemon.
What the operator must run
The app is served same-origin from account-api’s [static]
section, so the API base URL is simply the page’s own origin and no
CORS setup exists at all. Reading mail rides the /v1/mail surface,
which needs only the shared [store]. Compose (POST /v1/mail/send)
routes recipients the way sithbitd’s submission port does, so its reach
follows the API’s config:
- No extra config — bare base58 wallet addresses deliver locally; anything else answers 503 ( alias resolution needs the chain).
[chain]— aliases anduser@your-domainaddresses resolve through the gateway, with the frombox postage precheck applied before the message is accepted.[mail](local_domains,[mail.dkim]) — mirrors sithbitd’s submission settings: which domains are yours (everything else is relayed) and the DKIM keys relayed mail is signed with.
As with the other clients, the aliases/balances/keys settings panes
also need [chain]; login, mail reading, and the account settings work
without it.
Building and serving
cd webclients/webmail
./build.sh # wasm-pack build + stages shared/ into staging/
staging/ is the deployable bundle. Point account-api at it:
[static]
route = "/mail"
root = "webclients/webmail/staging"
then open http://127.0.0.1:8180/mail/index.html (or your deployment’s
origin).
Install as an app (PWA)
The webmail bundle is an installable progressive web app. Once it is served over its origin, the browser offers its native install prompt on the desktop (Chrome/Edge’s address-bar install button) and Add to Home Screen on mobile, giving you a standalone SithBit Mail window with its own icon and no browser chrome — tinted the brand teal.
A service worker precaches the static shell (HTML, CSS, JS, and the
wasm module), so the app loads offline — the login, unlock, and
dashboard frames paint with no network. Your mail itself stays live:
the service worker never caches the account API (/v1/*), so every
message, folder count, and send always hits the server. Open the app
with the server down and you get the shell; you reach your mail again
the moment it is back.
First run and onboarding
A brand-new visitor — no wallet stored in this browser yet — lands directly in the shared five-step onboarding wizard: create or import a wallet (the create branch shows your secret key once, with the save it — it is the only copy gate), claim an optional handle, set your default postage, review, and mint your mailbox on-chain. Until that finishes the app hides its mail and settings views and shows only the wizard. Every transaction is built and signed in the page by the wasm module; the server only relays it.
The app decides which of four views to paint from the session it probes on load:
- Onboarding wizard — no wallet stored yet, or an unlocked wallet that owns no mailbox (a returning user finishing setup).
- Unlock — a wallet is stored but locked this run; the app asks for its passphrase (the key is encrypted with that passphrase before it lands in the browser’s localStorage, and is asked for again each time you open the app).
- Dashboard — a fully set-up account: unlocked and mailbox-registered
— the normal
#/mailand#/settingsviews below. - (a brief loading view while it probes.)
Because the webmail bundle is served same-origin from account-api,
it needs no endpoint configuration at all: the API base URL is simply
the page’s own origin. The onboarding create relays through that same
API, so its on-chain reach follows the API’s [chain] config exactly as
the compose path does above.
Using it
The #/mail view is three panes. The folder rail lists your folders
with unseen counts; the middle column stacks per-folder search over the
message list (keyset-paged, newest first); the reader marks messages
read on open, toggles text/HTML/raw views (HTML render in a fully
sandboxed iframe that blocks scripts and remote loads), downloads
attachments, flags, moves, and deletes. A ✓ Verified trust mark
appears beside the From line when the sender holds an on-chain
verified-sender attestation
from its domain — here it checks the sender the (ingress-authenticated)
From header names, while the trustless viewer’s
badge binds the program-verified on-chain envelope signer. No mark
simply means “not attested”; it is never a warning. Compose floats bottom-right:
To/Cc take comma-separated addresses — aliases, user@domain, or bare
wallet addresses — and the reader’s Reply action opens it prefilled
(sender as To, Re: subject, threading header). A successful send
files an already-read copy in Sent and refreshes the folder counts;
recipient problems (unknown alias, no stamps on your frombox) surface
on the pane with your draft intact.
The #/settings view is the shared dashboard: mail password —
typed, or derived from a wallet signature with the pane’s Derive
mail password button (available while the wallet is unlocked; see
The mail password) —
timezone, do-not-disturb, aliases, balances, delegated
encryption-key management,
domain trading (list a domain you hold
for sale on the open marketplace, or buy a listed one by name and authority),
settling reply bounties (claim one on a
message you received and replied to, or refund one you placed that expired
unclaimed), pinning leases (escrow a
refundable deposit asking operators to keep a message’s pinned body past the
default retention — create
a lease by the message’s CID and id, check a CID for your wallet’s lease, or
close it
anytime to reclaim the deposit),
claiming and configuring your mailbox
(the handle, sending domain, default postage, and on-chain-only opt-out), and
closing the mailbox
— a two-step, 7-day timelocked flow, so the request only starts a
clock (the mailbox stays open and receiving, nothing is refunded), a
cancel is available throughout, and both rents return only when you come
back after the wait and finish the close. All of it behaves exactly as
documented for the Thunderbird extension. The
trustless viewer’s Lease this message button
(beside Reply, shown once a body has rendered) prefills the
pinning-leases create form with the open message’s CID and id and
routes the shell straight here — the URL hash becomes #/settings, so
the browser’s back button returns to the mail view. You still review
the deposit and submit, and entering a CID by hand works as before.
The compose loop is exercised headlessly by the env-gated live suite
webclients/webmail/test/e2e-webmail.test.js — the bundle served from
a real account-api, a real send to a throwaway wallet, and the message
read back from the recipient’s INBOX (the runbook lives in
webclients/README.md).