Repository navigation
Releases: sylvainHellin/mailypoppins
Releases · sylvainHellin/mailypoppins
Release list
v0.12.0
Added
- TUI themes
mailypoppinsandmailypoppins-light, the Mac app's palettes.theme = "mailypoppins"gives the TUI the app's Prussian ground, Cream text, Ocean focus and Pumpkin cursor;mailypoppins-lightis the Cream variant.mailypoppinsis the new default;theme = "catppuccin-mocha"brings back the old look. - A newly added account syncs on its own and turns ready without a daemon restart. When an account with a server and no local store comes into the daemon (at start or through a configuration change), the daemon runs its first quick sync in the background and then starts the account's runtime; a failed first sync leaves it
blockedwith the error and themp sync -A <account>that retries it. Amp syncthat creates an account's store also starts its runtime at once, where it used to wait for the next daemon start. - Graph accounts no longer print
Could not load SMTP configon every command. A Graph account sends through Graph and has no SMTP transport, so the CLI skips that load and its warning. - Graph accounts sync on their own (#0134). The runtime tick has a Graph body, so a Graph account is scheduled like an IMAP one, its inbox watcher's ticks run a sync, and a plain
mp syncgoes through the runtime. - Desktop: approve, back to draft, edit recipients and send work on a conversation's draft member (PERSO-146).
cA,cD,ceandxreach the draft member of an expanded conversation as they do in Drafts and in the TUI. - TUI: an expanded conversation member shows its To and Cc in the headers pane, and a draft member its own From, To and status (PERSO-146).
- Tests: golden frames for the
ttconversation overlay over a collapsed and an expanded conversation (PERSO-148). - Conversation listing in the daemon and mp-client (PERSO-138, steps 1 to 3).
message.list_conversationsstreams a mailbox as one row per conversation inmessage.conversationschunks,message.threadgainsprefer_mailboxandinclude_drafts, the drafts index keeps the message a draft answers or forwards, andSession/QueryHandlegainlist_conversationswithQueries::thread_expanded; no client shows it yet. - TUI and desktop: conversation view, on by default (PERSO-138). Every mailbox but Drafts lists one row per conversation with its participants, message count and a draft marker; Enter,
landhexpand it in place to its members across Inbox, Sent, Archive and Drafts, each labelled with its mailbox; actions on a collapsed row reach its messages in the listed mailbox only, and the list re-reads on any change in the account.tcand its palette entry switch an account back to one row per message, as does the desktop's "Conversation view" switch on the account's Settings card; the TUI remembers the choice in~/.config/mailypoppins/tui.json, the desktop in its local prefs. Both clients now requiremessage.list_conversationsat the handshake. examples/graph_answered_probe.rs(PERSO-171). Reads and writes Outlook's reply and forward properties on one message by Message-ID, and measures what the folder enumeration and the delta answer when asked for them.examples/graph_inline_probe.rs(PERSO-165). Measures which Graph attachment listing returnscontentIdwithout content, runs the inline backfill against a copy of an account's store, and prints what the readers make of one row.examples/graph_thread_probe.rs(PERSO-147). Measures which Graph calls return the threading headers andconversationIdand what they cost, and runs the Graph re-thread against a copy of an account's store.examples/draft_probe.rs(PERSO-139, slice 0). Appends a probe draft to an account's Drafts folder and dumps what Outlook and Apple Mail made of it, to pick the linking rule for two-way server drafts.tbon a draft opens it as it will be sent (PERSO-136). The newdraft.htmldaemon method renders the signature and the quoted reply through the samesend::draft_html_bodyasmp send. In the TUItbopens it in the browser; on the desktoptbdoes the same andtton a draft switches its preview between the Markdown body and that HTML in the sandboxed reader frame, with the message reader mode unchanged.- Desktop:
yon a draft copies its file path (PERSO-100). mp list-messages,mp searchandmp contacts searchtake--json. Each prints one JSON object on stdout with warnings left on stderr, so a script can parse the listing:{account, mailboxes, messages}forlist-messages,{account, query, source, messages}forsearch(the server or--local),{account, query, contacts}forcontacts search. A message carries its selector, mailbox, Message-ID, sender, recipients, subject, the date as a UTC ISO 8601 instant beside theDate:header, its flags and whether it has attachments;search --fulladds the body, and a server hit without--fullthe preview its text layout prints.mp searchnames the terms the local store answered on stdout (PERSO-122). On plain IMAPhas:attachmentis answered from the local store; the result header now sayslocal store only: has:attachmentand--jsoncarries"coverage": "server"|"local"with"local_terms", so a piped search no longer reads an un-synced gap as "no such mail". The stderr warning stays.- The TUI search overlay and the desktop search bar name the terms the local store answered (PERSO-131).
message.search_serversettles withcoverageandlocal_terms; both clients append "local store only: has:attachment" to the count when plain IMAP answered that term from the local store, asmp searchdoes. - Cancelling a running
sync.quickorsync.fullstops it (PERSO-127). The pass stops at the next body chunk and the next mailbox boundary; the mailboxes already ingested stay, the rest waits for the next pass, and nosync.completedis published. - A cancelled sync announces the mailboxes it did pull in (PERSO-133). The counts of every mailbox the pass ingested reach the other clients at once, still without a completed sync.
filename:ignores case beyond ASCII (PERSO-134).filename:übersichtfindsÜbersicht.pdf; accents still count and ß is not SS.- Desktop:
ffopens a Server Search form with the TUI's fields (PERSO-115). The form has scope, From, To, Subject, Keywords, After, Before, attachment and Advanced; the store answers first, then the server. - Desktop: an available update shows as a card at the bottom right with Install and Later (PERSO-97).
- Desktop:
eon a received message opens it read-only in$EDITORas Markdown, as in the TUI (PERSO-92). Enter focuses the reader.
Fixed
- Every account syncs once right after a daemon start or restart (PERSO-178). The scheduler's first sync came a whole interval (fifteen minutes by default) after the daemon started, and neither watcher syncs on its first look, so with no client asking a Microsoft 365 account stayed stale that long; on 2026-10-22 the evoqs account restarted at 12:54 first synced at 13:09. The first scheduled sync now runs ten seconds after an account comes up, unless a client's own startup sync finished first. The startup log line now reads
[scheduler] scheduling <account>: a first sync in 10s, then one after 900s without a sync, whereticking <account> after 900sread like a sync that never happened, andtickingis logged only for a sync that runs, with its reason. - A reply or forward sent from a Microsoft 365 account shows as answered in Outlook, and one made in Outlook shows in mailypoppins (PERSO-171). After a Graph send, the daemon records the reply, reply to all or forward on the source message in Exchange through the MAPI properties Outlook reads (
PidTagLastVerbExecuted, its time and the icon index), retried in the background like a move or a mark read; it was marked in the local store only. Graph sync reads the same property back: a message downloaded after a reply in Outlook lands answered, and a message already stored turns answered or forwarded on the next full folder listing, which a quick sync that walks the delta does not run. The listing costs about 6 bytes more a message. A history bit is only ever added, never cleared, because Exchange keeps the last verb alone. - Inline images show on Microsoft 365 mail, and signature logos are no longer attachments (PERSO-165, #0145). Ingest records each attachment's Content-ID and whether the HTML body shows it as an inline image, for IMAP and Graph alike. The desktop reader and the browser view now inline a Graph message's
cid:images from the stored attachments, where they rendered broken.mp showlists inline images under their ownInline images:heading after the real attachments, and the TUI picker and the desktop app list them after the real ones with aninlinetag; they stay reachable, somp savewrites them,mp openopens them when the message has nothing else (a phone photo), and hooks get them withinline: true. Forwards leave them out, and a message whose only parts are inline images shows no paperclip, while a Microsoft 365 message with an attached Outlook item or cloud link keeps its paperclip beside a stored logo. Acid:link to a document stays an attachment. Messages already stored are marked over the next syncs: IMAP rows from their raw message, Graph rows from an attachment list of names and content ids (about 1.3 KB a message), and Graph messages with only inline images stored before the Graph attachment fix fetch their images through the attachment repair. On the EVOQS store that marked 534 messages and fetched the images of 441. - A received reply on a Microsoft 365 account joins its conversation (PERSO-147). Graph downloads, including a server search hit fetched into the store, now carry
In-Reply-To,Referencesand the message'sconversationId, so a Graph row threads by its headers like an IMAP row, and a messag...
v0.11.0
Added
- The desktop app can update itself from GitHub Releases (#0139).
- At launch, at most once a day, the app checks for a newer version; the App menu's "Check for Updates…", the palette's "Check for updates" and the Settings line's "Check now" check at once and say the answer on the notice line.
- A newer version shows as "Update to X.Y.Z" at the sidebar's foot and in the palette; choosing it shows the download's progress on a card in the activity area, which then offers "Restart now" or "Later", asks about drafts still open in the embedded editor before a restart, and on a failure says why with a link to the release page.
- An update downloads, has its signature checked against the key the app ships with, and replaces
mailypoppins.appin place while the app and its daemon keep running; a restart then stops the daemon and opens the new version, which starts a daemon of its own version. - A release now carries the signed updater archive (
mailypoppins-desktop-aarch64-apple-darwin.app.tar.gzand its.sig) andlatest.json, and becomes the latest release only once that manifest is uploaded; a tag whose version differs fromCargo.tomlpublishes no manifest. - The app bundle is signed ad hoc until the Apple signing of #0012.
scripts/install-local.shpulls and installsmpand the desktop app from source on a Mac in one run:cargo install,pnpm bundlewith thatmpas the sidecar, the running app quit and the new one swapped into/Applications, one daemon restart (through launchd when the login agent is loaded), the app relaunched;--no-pull,--no-gui,--allow-dirty,--dry-run.mp send --invitesends all-day events and updates of an invitation already sent (#0140).- A bare date as
--start(--start 2026-12-24) sends an all-day event, which calendars show in the all-day row; without--endit is that one day. --endnames the last day, inclusive:--start 2026-12-24 --end 2027-01-08covers 24 December through 8 January, and--duration 3dcovers three days.- The preview shows the dates of an all-day event instead of a UTC instant.
--uid <UID> --sequence <N>re-sends an invitation as an update: give the UID of the one already sent and a higher sequence (the first send is 0), and calendar clients replace the event instead of adding a second one.send.invitetakes the optionalsequence.
- A bare date as
- The desktop client shows which pane has the focus, and
grfocuses the reader. The sidebar, the list or the reader, whichever the keys act on, has a thin line in the focus colour just inside its edge, in the dark and the light theme.grand the palette's "Focus reader" move the focus to the reader, besidegmfor the sidebar; from Contacts, Calendar or Settings the palette's row brings Mail back first. Tab and Shift+Tab still cycle the panes as in the TUI. - The desktop client's Save and Attach file dialogs open the system's folder and file picker (#0131). Browse beside the Directory and File fields opens the macOS picker; what you pick fills the field, with your home directory written
~, and typing a path still works. - A contact rebuild, an RSVP or an invitation being sent can be cancelled from the desktop client (#0131). While one runs, a card in the activity area shows it with Cancel; the daemon may still finish the rebuild or send the reply, and the notice says so.
- The daemon serves what the desktop client used to work out for itself (#0131).
- The desktop's Forward wizard picks a signature, as the New draft wizard does, and passes it to
draft.forward, which always took one. - The desktop's New draft wizard has the TUI's inline Body: a body typed there is written above the signature and the draft is done without the editor.
draft.createtakesbodyandheaders, so the daemon writes the whole file. - A
draft.listrow carries the draft'sbcc:, and the desktop'scefills its dialog from the listed row. draft.create,draft.reply,draft.forwardanddraft.create_from_messageanswer the subject the draft was written with.mp_client's session keeps a refusal'sdata(mp_client::session::refusal), so the desktop shows a broken draft's diagnostics as the daemon sent them.- A sent draft's outcome names its
Message-ID, whichsend.draftandsend.approvedanswered empty. - The daemon lists the signatures and an account's default (
signature.list), which the desktop's wizards read instead of the directory. - The daemon lists, attaches and removes a draft's attachments (
draft.attachments,draft.attach,draft.detach), with the checks the TUI'stamakes; the desktop's Attach file dialog and the preview's Remove go through it. - The desktop's Fetch of a server-only hit waits on the daemon's end-of-operation event instead of asking every 100 ms.
- The desktop's Forward wizard picks a signature, as the New draft wizard does, and passes it to
- The desktop client checks that the daemon runs its own version of mailypoppins (#0132).
- A daemon left running by another install of
mp, or by acargo installnot followed bymp daemon restart, now shows the restart screen naming both versions and thempthe app would start, instead of serving the window from an older engine; Restart replaces it with the matching one. - The same screen replaces the reconnecting banner when the daemon comes back as another version while the window is open.
- A daemon left running by another install of
- A tagged release builds the desktop app for the Mac, with its own
mpinside (#0132).- Each release now carries
mailypoppins-desktop-aarch64-apple-darwin.dmgfor Apple silicon, with the.appas a.tar.gzbeside it; dragmailypoppins.appto Applications and it starts its daemon with thempit ships, so nothing else has to be installed. - The app is not signed yet (#0012): allow it once under System Settings > Privacy & Security ("Open Anyway" after the first blocked launch), or run
xattr -dr com.apple.quarantine /Applications/mailypoppins.app. ln -s /Applications/mailypoppins.app/Contents/MacOS/mp /usr/local/bin/mpputs the app'smpon yourPATH, so the CLI, the TUI and the app run one version.- From a checkout,
cd clients/desktop && pnpm bundlebuilds the same app and DMG.
- Each release now carries
- The desktop client repeats a held key, keeps its colours in the editor, shows what a prefix key continues with, and completes recipients (#0137).
- A held
jorkrepeats in the list and in the embedded editor alike; a value you set yourself withdefaults writestill wins. - Neovim and Vim in the embedded editor take the app's own colours in the dark and the light theme, and follow a theme change at the next editor started; Settings' "Editor colours" keeps your own colorscheme instead, and every editor gets
MP_DESKTOP_THEMEfor a config of your own. - After a prefix key such as
corg, a small popup at the bottom lists the keys that can follow, titled by the family as in the TUI, and goes away on the next key or after 1.2 s. - To, Cc and Bcc in the compose dialogs complete your contacts as you type, as the TUI's wizard does.
- The highlighted row in the palette, the key help and the completion list is visible in the dark theme, and the app menu has Settings… on Cmd+,.
- A terminal editor in
MP_DESKTOP_EDITORor the editor setting, such asnvim, opensconfig.toml, the daemon's log, a signature and aninvite.icsin a terminal window, where before it started with no terminal at all.
- A held
- The desktop client runs your terminal editor inside the window (#0130). A draft you create, reply to, forward or edit opens in Neovim, Vim or Helix in the reader pane, with your own configuration and plugins, while the message list stays usable beside it; the editor comes from
MP_DESKTOP_EDITOR, the editor setting,$VISUALor$EDITOR, else the first ofnvim,vimandhxfound, also in the Homebrew directories, so a launch from the Finder finds them. Every save updates the list at once. Quitting with:wqshows the draft's preview,:q!drops only the unsaved changes and never the file, and an editor that crashes leaves the draft as last saved with a Reopen button. Selecting another message while the editor runs asks whether to keep editing in the background, close the editor or stay, and closing the window asks whether to close the editor or stay. A GUI editor such as VS Code or Zed still opens the draft in its own window. - The desktop client has a light theme, a plain-text reader, a filter in the key help, and runs a terminal editor in a terminal (#0136).
- Settings' Theme buttons and the palette's "Theme: dark", "Theme: light" and "Theme: system" switch the window between the dark palette, a light one on cream, and whichever the system uses; dark stays the default.
tt, the reader toolbar's HTML and Text, Settings' Reader field and the palette show a message's stored plain text instead of its HTML, in a monospaced block with quoted lines muted, and the choice is remembered; in the TUIttopens the thread, which the desktop does not have yet.- The
?key help has a filter field that narrows the keys as you type, by key, description or section, and keeps the sections in their usual order. - A terminal editor in
$VISUALor$EDITOR, such as nvim, vim or hx, now opens your drafts in the first terminal found among Ghostty, kitty, Alacritty, WezTerm and Terminal.app, where before it was skipped. - With
MP_DESKTOP_FIXTURE=1an editor handoff says the editor was not launched and names the command it would have run. - mailypoppins has a logo: an umbrella rising from an envelope, its hook below, a cream monoline on an ink tile with an orange ferrule. It is the desktop's app icon, the website's logo and favicon, and lives in
assets/logo/with a wordmark.
- The desktop client has the TUI's Calendar and Contacts views, manages signatures, and sets up accounts (#0131). The keys are the TUI's, each is also in the palette, and
MP_DESKTOP_FIXTURE=1has a simulation for eve...
v0.10.0
Added
mp completions <bash|zsh|fish|elvish|powershell>prints a shell completion script. Save it withmp completions zsh > "${fpath[1]}/_mp"or load it witheval "$(mp completions bash)"; it reads no config and never contacts the daemon.- The email list pages with
Ctrl+d/Ctrl+u(half page),PgDn/PgUp(full page) andHome/End. Sized to the visible list, clamped at both ends; the pre-daemontui-keys.jsoncapture is now a subset check rather than a byte pin. mp outbox list --jsonandmp list-mailboxes --jsonprint the listing as JSON on stdout.mp list --jsonprints the listing as JSON. The same rows the table shows, for a script to read without parsing columns.mp daemon restarttakes--grace-secslikemp daemon stop, andstartandrestartsay the daemon started. The grace is how long work already running may settle before the old daemon goes.- The global
-Areaches every per-account command.mp contacts,mp calendar rebuild,mp cutoverand themp configaccount commands lost their own--accountfor it;mp calendar rebuild,mp contacts rebuildandmp cutoverstill cover every account when it is absent. - A reply goes to the
Reply-To:mailboxes, every one of them, and falls back toFrom:. Reply-all keeps the original sender incc:and never repeats aReply-To:address there. - The terminal window is its own crate now, and it cannot reach your mail without asking the daemon (#0126). The last unit of the move that started five releases ago: everything the window is - the list, the keys, the panes, the overlays - is
crates/mp-tui, which links the shared configuration and parsing code, the wire types and the socket, and nothing else. It cannot open your store, your keyring or your mail server, and that is now a compile error rather than a rule someone has to keep. Nothing you press changes and nothing you look at moved: the twenty golden frames,mp --helpandmp dump-keys --jsonare byte-identical, and the same 2 414 tests run. One thing behaves differently in a corner nobody should ever reach: a window whose daemon connection wedged at startup now shows an empty list and an empty preview with a line in the log, where it used to read your store directly behind the daemon's back. - The conversation overlay, the reply to a hit only the server has, and the drafts list are the daemon's now (#0126). Nothing you press changes and nothing you look at moved, which is the point:
ttstill opens the thread a message belongs to,r,Randwon a search hit that is not in your store still quote what is on screen without downloading it, and the Drafts list still catches up when an agent or$EDITORwrites a file behind the application's back. What changed is who does the work. The conversation is folded where your store is, because which messages share a thread is a query over every mailbox of the account and the window holds one mailbox at a time. The reply to a server-only hit travels as the message rather than as an address, which is what keeps the reply key from silently downloading the message the way the overlay's separatefkey does. And the once-a-second scan of the drafts directory is gone: the daemon has watched every account's drafts directory for a while now and says so, so the list reacts to a write in any account rather than only the one you are looking at, and it no longer costs a directory walk four times a second whether anything changed or not. One thing was removed outright: the outbox counts the window read on every account open and every finished sync went nowhere, since the badge that showed them left with the old status bar. - The contacts refresh no longer freezes the window, and the agenda moved to where your mail lives (#0126). Pressing
rin the contacts view walked every message of every mailbox on the drawing thread: on a real account that is seconds of a window that does not repaint. It is a daemon operation now, so the view stays live while it runs and the same four lines you have always read appear when it finishes, the two refusals that protect a good cache from a thin rebuild included. Nothing else about it changed. Under the surface two more things left the TUI: the agenda the calendar view shows is built by the same code the daemon serves it from, in the crate that owns your store rather than in the client that displays it, and the invitation sourceEnteropens in$EDITORis read over the socket like every other read. The TUI's action layer now reaches your store, your mail server and the send path through daemon methods and through nothing else. - The search overlay fills in as the server answers, and the last thing the TUI dialled itself is the daemon's now (#0126). Pressing
ffused to show the local hits, then sit onSearching server...until every mailbox had answered and paint the lot at once, from an IMAP session the TUI opened on a thread of its own. The search runs in the daemon now and each hit lands as it arrives, so a slow folder no longer holds up the fast one, a message found both locally and on the server still appears once, and a folder the server refuses is reported at the end instead of losing you the other four.fon a server-only hit ingests it through the daemon too, and pressing it on a message that is already there says so rather than fetching it twice. Opening a message read-only in$EDITOR(Enter,e) and copying its path or itsmp://name (y) go the same way; the rendition is still the store's own Markdown, still read-only, and the copy is reclaimed when you leave the editor. Nothing you look at moved: the twenty golden frames,mp --helpandmp dump-keys --jsonare byte-identical.mp search's own server leg is deliberately not routed yet and still opens its own session. - The rest of the shared half followed it (#0126). The second of the three units that put the TUI behind a crate boundary, and like the first it changes nothing you run:
contacts,draftandreconcileare split at the line where they stop being about a file and start being about your store, the engine-free halves joincrates/mp-corebeside the rest, and the four RFC 5322 address-string helpers move out of the sending code that happened to own them. Every old path still resolves, the help surface and the key dump are byte-identical, and the test count did not move: 87 tests followed their code into the new crate and the guard that counts them was raised to say so. One thing did change under the surface: the draft preview asks the daemon where the file is and reads it itself, rather than opening your store to look the draft up. - The shared half of mp is its own crate now (#0126). Nothing you run changes and nothing you look at moved: this is the first of the three units that put the TUI behind a crate boundary, and all it does is move the modules the client and the engine both read - configuration, secrets, the OAuth2 token cache, signatures, RFC822 parsing, iCalendar, the search grammar, the
mp://selector grammar and the shared types - out of the root package intocrates/mp-core, re-exported under every path they had. The help surface, the key dump and the twenty golden frames are byte-identical, the test count did not drop, and the 329 tests that moved with the code are counted in their new home by the same guard that has watched the workspace layout since Phase 2. - The daemon can be left running now: it starts at login, holds the undo-send window itself, stops without dropping work, and can be asked how it is doing (#0125). Until this release the daemon was something a client started when it needed one. Four things change that. The undo-send window moved out of the TUI: pressing send parks the message for
email.send_hold_secsin the daemon rather than in whichever window you happened to send from, so the countdown is the same countdown in every window you have open,ucancels it from any of them, and quitting the TUI mid-window no longer refuses to quit. It cancels the send and leaves the draft approved, which is what closing the last window has always meant and now says so.mp sendandmp send-approvedstill send immediately, andsend_hold_secs = 0still opts out. Stopping is graceful:mp daemon stopcancels every armed hold, tells every connected client that the daemon is leaving, waits up to ten seconds (--grace-secs Nto change it) for work already running to settle, releases the account locks, and names on stdout anything it had to cut short.SIGTERMruns the same sequence, so a daemon stopped by the system at logout is as careful as one you stopped by hand, and a TUI whose daemon goes away saysDaemon stoppingand reconnects rather than dying with it.mp daemon install-servicewrites a systemd user unit on Linux or a launchd agent on macOS and enables it, so the daemon is up before you open anything;--checkreports whether one is installed,uninstall-serviceremoves it, and an install over a file you edited yourself is refused rather than overwritten. The macOS half is written and pinned against a committed fixture but has never been run against a reallaunchctl, which is the one part of this release that wants a careful first try. And the daemon answers questions about itself:mp daemon healthprints one line per account and one per check (the configuration file, the stores, the socket, the log) with a verdict and an exit code a script can branch on,mp daemon logstails the structured log with a level filter and no banner around it, andmp daemon support-bundlewrites a directory holding the configuration, the health report, the status, the version and the log with every password, client secret and token struck out of all five,secrets.encand the token cache never copied at all. A check whose status moves is announced to every open window, where it lands in the activity l...
v0.9.0
Added
- HTML-dominant mail renders as readable styled text in the preview, not a flat wrap (#0091). An HTML-only or HTML-dominant message used to reach the preview as plain text flattened at ingest and then re-parsed as Markdown, so its headings, lists, tables, links and emphasis collapsed into a single word-wrapped block. The preview now renders such a message's own HTML directly through html2text's rich interface, mapping its per-span annotations (strong, emphasis, links, code, CSS colours) onto the pane's styles, so the structure the sender wrote survives. Plain-only mail, drafts, and any message html2text cannot parse fall back to the previous rendering, so nothing regresses and the pane never blanks. The
b/tbopen-in-browser escape hatch is unchanged, for the mail a terminal still cannot lay out. No new dependency: html2text was already vendored for the ingest-time conversion, which is why the ticket's original "pick an external tool (w3m / lynx / pandoc)" premise was set aside in favour of the already-present crate, for zero-install portability across every release target. - A sent message is held briefly before hand-off, so it can be undone (#0090). Pressing send used to hand the message to SMTP the instant the confirm dialog closed, with no reprieve. A send in the TUI is now parked for
email.send_hold_secs(default 20 seconds) before it reaches the transport: the status line showsSending in Ns (press u to undo), anduin that window cancels the send outright, transmitting nothing and leaving the approved draft in place to send again or edit. When the window elapses the send proceeds exactly as before. The window is configurable under[email]; setsend_hold_secs = 0to opt out and send immediately, the previous behaviour. The hold sits ahead of the shared send path, so it covers both SMTP and Microsoft Graph accounts and both the plain send and the approve-and-send that the singlexkey performs. It is scoped to the interactive TUI;mp sendand the batch send-approved run are unchanged. - Each account can carry a signature, appended to the draft body on compose, reply, and forward (#0099). A signature used to exist only as an HTML file injected into the message at send time through a
{{SIGNATURE}}placeholder, invisible while you edited the draft and unusable for the common Markdown-first case. An account now defines its signature under[accounts.signatures]as a Markdown snippet, given inline withtextor by apathto a Markdown file (textwins when both are set), and it is spliced into the draft body when the draft is created: after the body for a new message, in the reply area above the quoted content for a reply or forward. The signature is therefore visible and editable in the draft rather than added on send. An account with no configured signature adds nothing, exactly as before, and the-s <name>/--no-signatureflags still pick or skip it. The{{SIGNATURE}}placeholder is kept in reply and forward drafts as the boundary the send path splits on to place the quoted original, but it no longer carries any signature text of its own. One-time consequence: a draft created before this change holds only the bare placeholder, so it now sends unsigned; re-create (or hand-sign) drafts that predate the upgrade. - A draft can gain an attachment from the TUI, no hand-edited YAML (#0098). Attaching to an existing draft was deferred at the data-layer rebuild, so the only way to add one was to open the draft
.mdin$EDITORand hand-edit itsattachments:frontmatter, then trust the paths at send time.t aon a Drafts row now prompts for a file path (in the same one-line slot the jump-to-date prompt borrows) and appends it to that draft'sattachments:list. The path is stored verbatim,~and all, so it is the very entrymp sendexpands and sends, the write counterpart of #0016'st o/t swhich resolve and open a draft's existing attachments. A path that is not on disk is named on the status line and the prompt stays armed, so a typo is a correction rather than a silent bad entry; the action is Drafts-only and says so elsewhere. - The compose wizard takes a short body inline, no
$EDITOR(#0097). Every message used to force the editor round-trip:nopened the wizard, submit launched$EDITORon the draft.md, and only there could a body be typed. The wizard now has a multi-line Body field after Subject, so a one-line reply can be typed and submitted without leaving the TUI.Enteron the body inserts a newline;Ctrl+gsubmits from anywhere. A non-empty body is written straight into the draft, which lands in Drafts under the normal approve-then-send lifecycle; leaving it empty keeps the old behaviour and opens$EDITORon the draft, still the fast path for long bodies or replies that quote other mail. The Body field is aNewcompose only: Forward always opens$EDITORon the quoted draft and Edit-recipients rewrites headers in place, so both skip it. - A command palette runs any action by name (#0100).
:orCtrl+popens a fuzzy finder over the runnable action catalogue: type part of an action's name,Enterruns it against the current context,Esccloses. It is the recall path for a forgotten chord, complementing the mnemonic families' fast path for a remembered one. The list derives from the sameKEYMAPthe help overlay, hint bar and website table read, so it needs no second action list and cannot drift; the held-back rows are the ones the palette cannot run (hand-dispatched overlay input, the family leaders, the key-reading view/mailbox/account jumps, and the palette opener itself). Typed characters are literal, so a family leader filters rather than arming a chord. - The header pane shows Bcc, Reply-To and an attachment marker, and its scroll is bounded (#0096). The headers pane was thin: it showed From, To, Cc, Subject and Date, and hid the rest. It now adds a Reply-To row and a Bcc row whenever the message carries those headers (blank values draw no row), and a paperclip
Attach:line whenever the message has attachments, so thet o/t sopen- and save-attachment actions that already work from the pane have a matching affordance. Thej/kscroll is now clamped to the wrapped content height, so it can no longer run past the last header into an empty void. Reply-To and Bcc are captured at ingest and reach the pane for stored (IMAP) mail; the schema bump refills the cache from the server on the next sync.
Performance
- The TUI paints before it opens any store (#0003). Startup used to build
every account's state up front, and eachAccountState::newopened that
account'sstore.sqlite3to read its counts and outbox; the first open of a
file runs a fullPRAGMA integrity_check(~240 ms on a 44 MB store), so five
accounts meant ~1.2 s of blank terminal before the first frame. The shell now
paints immediately with zeroed counts and a··loading marker, and the
per-account store opens (with their integrity checks and, on failure, the
drop-and-rebuild path) run on background threads after the first frame. Each
account fills in its counts, outbox badge and, for the active account, its
open mailbox as its store finishes opening; the startup auto-fetch is
sequenced after the open so a sync never races the first open of the same
file. Switching to an account whose store is still opening shows an
Opening …status instead of a wrong empty list, and never blocks the UI. - The preview body is wrapped once, not every frame (#0093). Parsing the
inline markdown and word-wrapping the whole message into styled lines used to
run on every render, so every scroll keystroke and every idle tick redid work
proportional to the body length. The styled lines are now memoised, keyed by
the body content, the pane width and the inline-image set, and only the
scrolled window is rendered, so a scroll costs the visible height rather than
the whole body. The cache rebuilds on a selection move, an async body
arrival, a re-ingest under the cursor, and a terminal resize. - The TUI redraws only when something changed (#0093). The event loop
calledterminal.drawon every iteration, roughly four times a second at
idle, rebuilding all widget content each tick. It now tracks a dirty flag and
skips the draw when nothing moved; input, resize, watcher events, background
results and drafts changes all mark the frame dirty, and the busy spinner
keeps its own slow tick, so idle CPU drops toward zero without stalling any
background update. - The mailbox listing is served by an index, not a sort (#0094). Loading a
mailbox filtered by(account, mailbox)and sorted bydate_sort DESC, id DESCwith no index behind the sort, so SQLite built a temp B-tree on every
load, and each row paid a correlatedEXISTSsubquery for its invite badge.
A newmessages_listindex on(account, mailbox, date_sort DESC, id DESC)
lets the listing walk straight off the index, and the invite flag now comes
from a deduplicatedLEFT JOINrather than a per-row subquery, so a mailbox
switch or a reload is one index scan with no temp sort. The listing content
is unchanged; the schema version bumps to 7, which rebuilds existing stores
from their cache on the next open. - Network actions share one tokio runtime instead of building a fresh one
each (#0095). Every background sync, send and search, plus the IMAP and
Graph watcher threads, used to calltokio::runtime::Runtime::new()on their
own thread and tear the whole runtime down when the op finished, spinning up
a worker thread per core and a blocking pool every time. They nowblock_on
a single lazily-built multi-thread runtime that lives for the process, so an
action pays the work and not the thread-pool churn. Blocking semantics are
unchanged: each call still blocks its own OS thread, and no pat...
v0.8.0
v0.6.0
v0.5.0
v0.4.1
added a config command. config is now managed globally from a central folder + secret manager of the os for passwords - no more .env file needed. Walk through config process added.
v0.4.0
added a config command. config is now managed globally from a central folder + secret manager of the os for passwords - no more .env file needed. Walk through config process added.
v0.3.0
Refactor into multi-module architecture: split main.rs into 8 focused modules. Removed browse command (superseded by beautifulmail TUI.