Skip to content

Releases: sylvainHellin/mailypoppins

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 22 Oct 13:51

Added

  • TUI themes mailypoppins and mailypoppins-light, the Mac app's palettes. theme = "mailypoppins" gives the TUI the app's Prussian ground, Cream text, Ocean focus and Pumpkin cursor; mailypoppins-light is the Cream variant. mailypoppins is 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 blocked with the error and the mp sync -A <account> that retries it. A mp sync that 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 config on 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 sync goes through the runtime.
  • Desktop: approve, back to draft, edit recipients and send work on a conversation's draft member (PERSO-146). cA, cD, ce and x reach 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 tt conversation 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_conversations streams a mailbox as one row per conversation in message.conversations chunks, message.thread gains prefer_mailbox and include_drafts, the drafts index keeps the message a draft answers or forwards, and Session/QueryHandle gain list_conversations with Queries::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, l and h expand 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. tc and 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 require message.list_conversations at 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 returns contentId without 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 and conversationId and 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.
  • tb on a draft opens it as it will be sent (PERSO-136). The new draft.html daemon method renders the signature and the quoted reply through the same send::draft_html_body as mp send. In the TUI tb opens it in the browser; on the desktop tb does the same and tt on a draft switches its preview between the Markdown body and that HTML in the sandboxed reader frame, with the message reader mode unchanged.
  • Desktop: y on a draft copies its file path (PERSO-100).
  • mp list-messages, mp search and mp contacts search take --json. Each prints one JSON object on stdout with warnings left on stderr, so a script can parse the listing: {account, mailboxes, messages} for list-messages, {account, query, source, messages} for search (the server or --local), {account, query, contacts} for contacts search. A message carries its selector, mailbox, Message-ID, sender, recipients, subject, the date as a UTC ISO 8601 instant beside the Date: header, its flags and whether it has attachments; search --full adds the body, and a server hit without --full the preview its text layout prints.
  • mp search names the terms the local store answered on stdout (PERSO-122). On plain IMAP has:attachment is answered from the local store; the result header now says local store only: has:attachment and --json carries "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_server settles with coverage and local_terms; both clients append "local store only: has:attachment" to the count when plain IMAP answered that term from the local store, as mp search does.
  • Cancelling a running sync.quick or sync.full stops 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 no sync.completed is 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:übersicht finds Übersicht.pdf; accents still count and ß is not SS.
  • Desktop: ff opens 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: e on a received message opens it read-only in $EDITOR as 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, where ticking <account> after 900s read like a sync that never happened, and ticking is 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 show lists inline images under their own Inline images: heading after the real attachments, and the TUI picker and the desktop app list them after the real ones with an inline tag; they stay reachable, so mp save writes them, mp open opens them when the message has nothing else (a phone photo), and hooks get them with inline: 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. A cid: 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, References and the message's conversationId, so a Graph row threads by its headers like an IMAP row, and a messag...
Read more

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 14:12

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.app in 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.gz and its .sig) and latest.json, and becomes the latest release only once that manifest is uploaded; a tag whose version differs from Cargo.toml publishes no manifest.
    • The app bundle is signed ad hoc until the Apple signing of #0012.
  • scripts/install-local.sh pulls and installs mp and the desktop app from source on a Mac in one run: cargo install, pnpm bundle with that mp as 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 --invite sends 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 --end it is that one day.
    • --end names the last day, inclusive: --start 2026-12-24 --end 2027-01-08 covers 24 December through 8 January, and --duration 3d covers 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.invite takes the optional sequence.
  • The desktop client shows which pane has the focus, and gr focuses 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. gr and the palette's "Focus reader" move the focus to the reader, beside gm for 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.create takes body and headers, so the daemon writes the whole file.
    • A draft.list row carries the draft's bcc:, and the desktop's ce fills its dialog from the listed row.
    • draft.create, draft.reply, draft.forward and draft.create_from_message answer the subject the draft was written with.
    • mp_client's session keeps a refusal's data (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, which send.draft and send.approved answered 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's ta makes; 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 client checks that the daemon runs its own version of mailypoppins (#0132).
    • A daemon left running by another install of mp, or by a cargo install not followed by mp daemon restart, now shows the restart screen naming both versions and the mp the 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 tagged release builds the desktop app for the Mac, with its own mp inside (#0132).
    • Each release now carries mailypoppins-desktop-aarch64-apple-darwin.dmg for Apple silicon, with the .app as a .tar.gz beside it; drag mailypoppins.app to Applications and it starts its daemon with the mp it 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/mp puts the app's mp on your PATH, so the CLI, the TUI and the app run one version.
    • From a checkout, cd clients/desktop && pnpm bundle builds the same app and DMG.
  • 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 j or k repeats in the list and in the embedded editor alike; a value you set yourself with defaults write still 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_THEME for a config of your own.
    • After a prefix key such as c or g, 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_EDITOR or the editor setting, such as nvim, opens config.toml, the daemon's log, a signature and an invite.ics in a terminal window, where before it started with no terminal at all.
  • 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, $VISUAL or $EDITOR, else the first of nvim, vim and hx found, also in the Homebrew directories, so a launch from the Finder finds them. Every save updates the list at once. Quitting with :wq shows 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 TUI tt opens 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 $VISUAL or $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=1 an 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=1 has a simulation for eve...
Read more

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 24 Sep 12:49

Added

  • mp completions <bash|zsh|fish|elvish|powershell> prints a shell completion script. Save it with mp completions zsh > "${fpath[1]}/_mp" or load it with eval "$(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) and Home/End. Sized to the visible list, clamped at both ends; the pre-daemon tui-keys.json capture is now a subset check rather than a byte pin.
  • mp outbox list --json and mp list-mailboxes --json print the listing as JSON on stdout.
  • mp list --json prints the listing as JSON. The same rows the table shows, for a script to read without parsing columns.
  • mp daemon restart takes --grace-secs like mp daemon stop, and start and restart say the daemon started. The grace is how long work already running may settle before the old daemon goes.
  • The global -A reaches every per-account command. mp contacts, mp calendar rebuild, mp cutover and the mp config account commands lost their own --account for it; mp calendar rebuild, mp contacts rebuild and mp cutover still cover every account when it is absent.
  • A reply goes to the Reply-To: mailboxes, every one of them, and falls back to From:. Reply-all keeps the original sender in cc: and never repeats a Reply-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 --help and mp dump-keys --json are 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: tt still opens the thread a message belongs to, r, R and w on 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 $EDITOR writes 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 separate f key 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 r in 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 source Enter opens in $EDITOR is 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 ff used to show the local hits, then sit on Searching 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. f on 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 its mp:// 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 --help and mp dump-keys --json are 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, draft and reconcile are split at the line where they stop being about a file and start being about your store, the engine-free halves join crates/mp-core beside 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 into crates/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_secs in 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, u cancels 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 send and mp send-approved still send immediately, and send_hold_secs = 0 still opts out. Stopping is graceful: mp daemon stop cancels every armed hold, tells every connected client that the daemon is leaving, waits up to ten seconds (--grace-secs N to change it) for work already running to settle, releases the account locks, and names on stdout anything it had to cut short. SIGTERM runs 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 says Daemon stopping and reconnects rather than dying with it. mp daemon install-service writes a systemd user unit on Linux or a launchd agent on macOS and enables it, so the daemon is up before you open anything; --check reports whether one is installed, uninstall-service removes 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 real launchctl, which is the one part of this release that wants a careful first try. And the daemon answers questions about itself: mp daemon health prints 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 logs tails the structured log with a level filter and no banner around it, and mp daemon support-bundle writes 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.enc and 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...
Read more

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 20:34

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 / tb open-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 shows Sending in Ns (press u to undo), and u in 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]; set send_hold_secs = 0 to 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 single x key performs. It is scoped to the interactive TUI; mp send and 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 with text or by a path to a Markdown file (text wins 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-signature flags 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 .md in $EDITOR and hand-edit its attachments: frontmatter, then trust the paths at send time. t a on 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's attachments: list. The path is stored verbatim, ~ and all, so it is the very entry mp send expands and sends, the write counterpart of #0016's t o / t s which 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: n opened the wizard, submit launched $EDITOR on 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. Enter on the body inserts a newline; Ctrl+g submits 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 $EDITOR on the draft, still the fast path for long bodies or replies that quote other mail. The Body field is a New compose only: Forward always opens $EDITOR on the quoted draft and Edit-recipients rewrites headers in place, so both skip it.
  • A command palette runs any action by name (#0100). : or Ctrl+p opens a fuzzy finder over the runnable action catalogue: type part of an action's name, Enter runs it against the current context, Esc closes. It is the recall path for a forgotten chord, complementing the mnemonic families' fast path for a remembered one. The list derives from the same KEYMAP the 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 the t o / t s open- and save-attachment actions that already work from the pane have a matching affordance. The j/k scroll 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 each AccountState::new opened that
    account's store.sqlite3 to read its counts and outbox; the first open of a
    file runs a full PRAGMA 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
    called terminal.draw on 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 by date_sort DESC, id DESC with no index behind the sort, so SQLite built a temp B-tree on every
    load, and each row paid a correlated EXISTS subquery for its invite badge.
    A new messages_list index on (account, mailbox, date_sort DESC, id DESC)
    lets the listing walk straight off the index, and the invite flag now comes
    from a deduplicated LEFT JOIN rather 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 call tokio::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 now block_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...
Read more

v0.8.0

Choose a tag to compare

@sylvainHellin sylvainHellin released this 08 Apr 21:58

Full Changelog: v0.6.0...v0.8.0

v0.6.0

Choose a tag to compare

@sylvainHellin sylvainHellin released this 30 Mar 15:54

Full Changelog: v0.5.0...v0.6.0

v0.5.0

Choose a tag to compare

@sylvainHellin sylvainHellin released this 08 Mar 17:37

Full Changelog: v0.4.1...v0.5.0

v0.4.1

Choose a tag to compare

@sylvainHellin sylvainHellin released this 01 Mar 22:23

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

Choose a tag to compare

@sylvainHellin sylvainHellin released this 01 Mar 22:17

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

Choose a tag to compare

@sylvainHellin sylvainHellin released this 26 Feb 15:30

Refactor into multi-module architecture: split main.rs into 8 focused modules. Removed browse command (superseded by beautifulmail TUI.