Repository navigation
Releases
Releases · bchoor/mailcellar
v0.0.18
Key takeaways
- [Feature] [Security] Always show images from senders you trust: In the message panel, the "Show images" button now has a ▾ menu to always show images from a sender's domain or exact address, but only when Gmail confirmed the message really came from that sender.
- [Fix] Gmail sync restarts itself after a dropped run: Background Gmail sync for every connected account now re-schedules its next run if Cloudflare dropped it, so new mail keeps arriving in your archive without you doing anything.
- [Fix] Messages page shows all columns again: On the Messages page, the table now shows all seven columns (Date, Sender, Subject, Category, Labels, Size, Attachments) even with the sidebar and message panel open, and scrolls sideways when it is too narrow.
- [Improvement] Open tabs pick up new versions: In any MailCellar tab left open across an update, the page reloads onto the new version when its files are gone, or offers a Reload button when a new version is out.
- [Fix] People shows "App access": On the People page, the access column is now labeled "App access" instead of "MailCellar", and the Admins tile reads "Can’t remove the last admin".
App
- The message panel's "Show images" menu offers "Always show images from" the sender's main domain (
mail.example.combecomesexample.com) or the exact address. Shared mail domains such as gmail.com offer the address only. - Mail from an allowed sender opens with images shown, an "Allowed: example.com" label and one icon to stop. Allowing and stopping both show an Undo toast.
- Senders Gmail could not confirm get a one-time "Show" only, with a line explaining why.
- Every image you choose to show now loads through MailCellar instead of straight from the web. The mail can't reach outside sites directly, and the browser doesn't send cookies or a referrer.
- On the Messages page, the open row no longer draws a focus ring over its highlight while you step through messages with ↑ and ↓. A focused row that isn't open keeps its ring.
- Requests that hang now show an error after 15 seconds for reads and 60 seconds for writes, instead of waiting forever.
- The interface kit is updated, which also keeps the sideways scroll position steady when a row opens so Date doesn't slide out of view.
Gmail
- Remembered image senders count only when Gmail's own check passed for the From domain. A pass on the sender's SPF record alone is not enough.
Sync
- A scheduled sync that is missing, or more than a minute overdue, is treated as dead and set up again when the mailbox starts or you visit.
- If a sync run fails, it now retries an hour later instead of exhausting its retries and being dropped. This is what stopped Gmail sync after Cloudflare's daily free-tier limit was hit.
- On the first start after this release, each Gmail account runs a normal incremental sync to catch up. There is no full backfill.
- One-time maintenance work (filling labels, refilling search, cleaning Spam and Trash) now runs in small steps after the mailbox starts, so it can't hold up your first request.
Storage
- Remembered image senders are saved per user, up to 2,000.
- Database writes now name their columns, so an older version keeps working after an update adds a column and a rollback is safe.
Infra
- Telemetry now runs on the shared Lumen ops tooling. It records requests, Durable Object and R2 use, Gmail calls and refusals, and browser views, failed requests, Web Vitals and crashes. Responses also carry
Server-Timing. - The old telemetry dataset and its binding are removed. Nothing is lost, and past data stays queryable until it ages out.
- Updates are now checked with an upgrade test before merge, plus a live check with rollback after deploy.
- CI now runs on OpenShip direct instead of GitHub's job queue, with the same jobs and steps. The CI cache is turned off because OpenShip direct has no cache server, which saves about 3 minutes per run.
Pull requests (9)
- #183 infra: run CI on OpenShip direct, not through GitHub's job queue (@bchoor)
- #182 fix: Messages shows every column with the sidebar and message panel open; kit 0.1.168 (@bchoor)
- #181 feat: remember senders whose images show, only when Gmail confirmed them (@bchoor)
- #180 fix: Gmail sync re-arms an alarm Cloudflare dropped; a failed alarm retries in an hour (@bchoor)
- #179 infra: drop the old mailcellar_telemetry points; mailcellar_requests is confirmed live (@bchoor)
- #178 fix: People's access column reads "App access"; kit 0.1.129 (@bchoor)
- #177 infra: telemetry on Lumen ops (mailcellar_requests), Server-Timing, /api/t and /api/client-error (@bchoor)
- #176 infra: nothing on start may stall prod; e2e upgrade test before merge; live check with rollback after deploy (@bchoor)
- #175 fix: bump the kit to 0.1.126; the open row drops its focus ring (@bchoor)
Full changelog: v0.0.17...v0.0.18
v0.0.17
Key takeaways
- [Feature] Read messages in a side panel: On the Messages page, clicking an email now opens it in a panel beside the list, and clicking another row swaps it in place, with the open row highlighted and ↑/↓ moving to the next email.
- [Feature] Messages per year, split by labels: On the Overview page, the Messages per year chart now stacks Labelled and Unlabelled mail to the yearly total, and you can tap the legend to see either part alone.
- [Feature] Jump to any page of messages: At the bottom of the Messages page, a new footer shows which messages you're viewing, page numbers, and a Go to box so you can open any page directly.
- [Fix] No more blank app on load: When the app starts, it no longer sits on an empty page if your browser's saved data is slow or stuck, and it loads from the server instead.
- [Fix] Searches with no results can be cleared: On the Messages page, a search or filter that matches nothing now keeps the filter box, column headers and a Clear filters button, and the filter menu no longer shows false 0 counts.
App
- The message panel can be dragged wider or narrower, folded into a thin rail, and remembers its size and fold state. On phones it is still a bottom sheet.
- With the panel open, the Messages table drops its lowest-priority columns instead of scrolling sideways. Subject stays next to Date, and every column returns when the panel closes.
- Emails wider than the panel zoom out to fit, and plain-text emails wrap.
- Category tabs that don't fit the row, such as on a phone or beside the panel, now fold into a More ▾ menu instead of being cut off.
- The Overview chart's top axis label no longer sits low, so the 2026 bar reads as the correct 17.3K instead of 18K.
- The Messages footer shows
1,001–1,050 of 3,243with page numbers on desktop, and a compactPage n of 65pager on phones.
Gmail
- Labelled mail means a message with at least one of your own labels. Inbox, Starred, Important and the categories don't count, and mbox imports count as well.
Search
- With a filter on and no matches, the "No mail matches these filters" message appears under the headers, with the filter and chips still in place.
- The filter menu hides counts while only one page of results is loaded, rather than showing a misleading number.
- Jumping to a page is cheapest when sorted by date with no filter. Sender, Size and category or label views read from whichever end is nearer. Most other filters still reach only the pages you've already visited, plus the next.
Sync
- On the Copy mail page, Jobs (the queue by month, filters, Pause, Clear queue and Finished) is now its own card at the bottom, with its own Refresh. It follows the Mailbox chip like the other cards.
- The Copy mail card now holds only the totals, Running now and the tiles.
- A month that is fully copied but still waiting to finish now shows Finishing under Left instead of an empty "Copying". If no rate is available yet, it shows a dash.
Storage
- Each month's labelled count is stored in the monthly totals that are already updated, so syncing and page loads cost no extra reads or writes.
- On first start, existing mail is counted once, in a single pass. It was tested on a 120k-message copy with no mismatches, in about 150 ms. Because labels aren't changed after a message is stored, the count can't drift.
Infra
- Saved answers now use the shared UI kit's version, so every app gets the 1.5-second give-up. They keep the same database, so existing saved answers carry over.
- An aborted or blocked browser cache now fails cleanly instead of hanging.
- The UI kit is updated to 0.1.125 across these changes.
Pull requests (10)
- #174 feat: Messages per year shows labelled and unlabelled mail, stacked to the total (Lumen 0.1.125) (@bchoor)
- #173 fix: Messages marks the open email's row (Lumen 0.1.124) (@bchoor)
- #172 infra: Lumen 0.1.122 (tab rows fold overflow into More) (@bchoor)
- #171 fix: saved answers use Lumen's savedAnswers; Lumen 0.1.121 (@bchoor)
- #170 fix: a search that matches nothing keeps the filter, headers and Clear filters; no fake 0 counts (Lumen 0.1.120) (@bchoor)
- #169 fix: beside the docked panel the message list fits (Subject kept) and wide emails zoom to fit; Lumen 0.1.119 (@bchoor)
- #168 feat: Messages page footer; jump to any page (Lumen 0.1.117) (@bchoor)
- #167 feat: Messages opens a message in a docked side panel; next row swaps it; Lumen 0.1.116 (@bchoor)
- #166 fix: the app never stays blank waiting on the saved answers (@bchoor)
- #165 feat: Jobs is its own card, last on Copy mail (@bchoor)
Full changelog: v0.0.16...v0.0.17
v0.0.16
Key takeaways
- [Feature] One mailbox picker in the sidebar: With two or more mailboxes, the sidebar now shows a single "Mailbox" selector above the chosen mailbox's pages, so you can switch mailboxes and stay on the page you're on.
- [Improvement] Phone tables read as cards, and sizes use their own unit: On phones, the Senders, Labels and Messages tables show two-line cards that fit the screen, and size columns now read like 812 KB, 45.2 MB or 4.5 GB.
- [Improvement] Friendlier times in your own time zone: In the Messages table's Date column, today's mail shows its time, then "Yesterday", then the date, and times elsewhere read like "1:31 AM PT".
- [Fix] Label chips are readable in light mode: Label chips now pick dark or light text so it stays readable on their Gmail colour, so the orange "billing" chip no longer has white text.
- [Performance] Pages open faster on reload: Every page now paints at once from your last answers and your browser keeps the app's files, so reloading no longer waits on the server.
App
- The What's new drawer shows only each release's key takeaways, with no raw change titles.
- On phones, Messages lists Subject first, then From. Secondary columns are hidden so numbers fit: name and first email on Senders, type on Labels, category, labels and attachments on Messages.
- A message with no attachments shows a dash instead of 0.
- A subscription with no display name shows the start of its address instead of a blank.
- Numbers use the text font with even-width digits. Monospace stays for code and the hash.
- The status bar reads "Mail · Up to date", and the time zone chip says PT.
- People, audit and schedule times follow your time zone, for example "Daily at 8:15 PM PT".
- The last mailbox you picked is remembered on your device. On a Gmail-only page, switching to an archive falls back to Overview.
Gmail
- Gmail system labels such as
YELLOW_STARnow read "Yellow star". - Label chips follow the Gmail colour with text chosen for readability.
Sync
- Copying a mailbox, live sync and import now write sender, domain and label totals once per 2,000 messages instead of every batch.
- This cuts writes per message from about 7.8 to about 5, so the free tier's daily write limit lets you copy more mail a day.
- Month counts still update batch by batch, so progress keeps moving.
Storage
- Saved totals are written out before anything reads or removes them, so archive pages, groups, label lookups and spam removal always see current numbers.
- A rolled-back import re-reads the waiting totals, so memory never runs ahead of storage.
- A mailbox keeps its 8 most recent archive answers until its next write, so repeat views skip re-reading the same rollups.
- The app saves its answers in the browser (not message bodies, attachment text or search snippets). They are refreshed when mail changes, and cleared on a new build, on sign-out, or when a different person signs in.
Infra
- The usage page answers right away from the last Cloudflare read and refreshes in the background. Only the very first read and Refresh still wait.
- Built app files are cached for a year, while the main page is still checked on each load so a deploy shows at once.
- Shared icons load as one file, so the number of files preloaded before first paint drops from 43 to 10.
- Updated the Lumen UI kit to 0.1.109 in both the web app and the worker.
- CI jobs can run on either available runner, so they no longer queue behind one.
- The release takeaways prompt now leaves out CI, build, test and internal-only changes.
Pull requests (10)
- #164 fix: What's new shows each release's key takeaways only, never PR titles (@bchoor)
- #163 fix: label chips readable in light mode; Lumen 0.1.109 (@bchoor)
- #162 feat: phone tables as cards; numbers in the text font; Messages shows today's times (@bchoor)
- #161 perf: copying writes sender, domain and label totals once per 2,000 messages (@bchoor)
- #160 perf: usage page answers from the last Cloudflare read, refreshes in the background (@bchoor)
- #159 feat: sizes in their own unit; tidier tables on phones and in lists (@bchoor)
- #158 perf: pages paint from saved answers; a mailbox keeps repeat answers (@bchoor)
- #156 ci: run on any OpenShip runner (@bchoor)
- #157 perf: browsers keep the app's files; shared icons load as one file (@bchoor)
- #155 feat: one mailbox picker in the sidebar; subject first on phones (@bchoor)
Full changelog: v0.0.15...v0.0.16
v0.0.15
Key takeaways
- [Feature] Search inside attachments: On the Messages page, free-text search now also finds words inside PDF, Word, Excel, PowerPoint and text attachments, and shows a hit as "In budget.csv" under the subject.
- [Fix] Imported messages starting with 0 now appear in search: On the Messages page, imported (mbox) messages whose ID begins with 0, about 1 in 16, now show up when you search their subject, sender, labels or body.
- [Security] Removed people are signed out of Cloudflare Access at once: In Admin ▸ People, when you remove someone, their Cloudflare session now ends immediately, so their next request goes back to sign-in, where they are no longer allowed.
- [Fix] Sizes, times and counts read the same everywhere: On the Overview page, the Senders page and the status bar, sizes use one decimal format, times follow your time zone preference, and the Senders tile shows the real count instead of stopping at 1,000.
- [Fix] [Performance] Messages with many attachments open reliably: When you open a message in the mailbox, a message with dozens of attachments now loads its attachment previews without errors.
App
- Sizes, times and counts now use one format on every page, so the same bytes no longer read 12.2 GB on one page and 13.07 GB on another.
- Sizes use decimal units (MB, KB), as storage is billed and macOS shows them.
- Times follow the time zone preference instead of the device's zone or a fixed Pacific time.
- The Senders tile counts all senders and domains instead of the length of a list capped at 1,000.
- Overview tiles show a dash until they have loaded, never a false 0.
- The status bar now says "Mail · Up to date", or "Copying 42%" while copying. It no longer says "Copying · Live" beside a page saying "Not copying".
- The app's JavaScript download shrank from 776 KB to 418 KB gzipped. The edit dialog, charts, What's new and markdown now load only when opened.
- Account pages make one trip to the database instead of three, which cuts typical load times. A just-added account is found at once.
Gmail
- Archive views no longer read the Gmail sign-in first. Only Gmail's own pages (labels, filters, subscriptions) do.
- Gmail message IDs were never affected by the search ID bug.
Search
- Free text now matches the envelope, the body or attachment text. This applies on the Messages page, the old archive view, the message count and MCP
search_messages. - Attachment search covers PDF, Word, Excel, PowerPoint, OpenDocument and txt/csv/md/html files. Images and calendar files are skipped.
- Files over 10 MB are skipped. At most 6 files per message and 32 KB of text per file are indexed.
- The attachment text is an index only. The files stay in your raw files.
- Search no longer misses imported messages whose ID starts with 0. The index itself was always correct, so no data change or migration is needed.
Sync
- Each sync and each finished mbox import now starts an attachment pass for new mail. It stays within Free-tier CPU, request and daily write limits, then continues.
- Searching attachments in older mail is a one-time pass that does not start on its own. It waits for the owner's go-ahead.
- Opening a message and previewing its attachments now happens in the mailbox, which keeps the last parsed message. A burst of previews for one message parses it once instead of re-reading it each time.
- Workflow telemetry no longer breaks sync runs. Imports run to completion.
Storage
- Sender, domain and label totals now move by each message's share whenever mail is added or removed, through sync, copy, import, spam removal or month un-indexing.
- The index no longer recounts every message after each month. It recounts once per mailbox when the rollup version changes, then never again.
- This stops the heavy row reads that used up much of the Free tier's 5M/day (one hour read 1.16M rows).
- The mailbox keeps the senders and domains count until the message count changes.
Infra
- Every request, Durable Object call and sync step now writes one data point to Analytics Engine, covering time taken, rows read and written, file operations and Gmail calls. The point is to show which call costs what.
- Removing someone in Admin ▸ People now also revokes their Cloudflare Access session. If a revoke fails, the sync status names who could not be signed out.
CF_ACCESS_TOKENnow also needs the Access: Organizations, Identity Providers, and Groups Edit permission.- Attachment text extraction uses a new Workers AI binding, which needs no secret.
- Lumen packages moved from 0.1.87 to 0.1.89, pinned together.
Pull requests (10)
- #154 fix: one way to write sizes and times; counts never capped or zero while loading (@bchoor)
- #153 perf: an account's page takes one trip to the database, not three (@bchoor)
- #152 fix: open messages and their attachments in the mailbox, parsed once (@bchoor)
- #151 perf: sender, domain and label totals move by shares; no recount after each sync (@bchoor)
- #150 fix: telemetry calls Workflow steps as methods (sync would fail) (@bchoor)
- #147 feat: search inside attachments (PDF, Word, Excel, PowerPoint, text) (@bchoor)
- #149 perf: Lumen 0.1.89, app JS 776 → 418 KB gz (@bchoor)
- #148 fix: search finds imported messages whose ID starts with 0 (@bchoor)
- #145 infra: per-call telemetry in Analytics Engine (@bchoor)
- #146 feat: sign people out of Cloudflare Access when they are removed (@bchoor)
Full changelog: v0.0.14...v0.0.15
v0.0.14
Key takeaways
- [Feature] Public landing page, with the app moved to /app: At the site's root address, visitors now see a public page with the film, a how-to video, costs and open-source info, while the app itself lives under /app and old links redirect there.
- [Feature] Live running jobs and finished-job history: On the Sync card, a new "Running now" list shows each sync and backfill in progress with what it is doing and a ticking timer, and the renamed Jobs card (formerly Backfills) has a Finished filter for past results.
- [Feature] Invited people can now sign in: In Admin ▸ People, every invite, removal or access change now updates the Cloudflare Access rule so invited people can get a sign-in code. The page shows the sync status.
- [Improvement] Email linked to FreeSend over the tunnel: In Settings ▸ Email, the Private tunnel route now defaults to the FreeSend server's name, the badge reads "Linked" or "Not linked to MailCellar", and Send test shows the email server's own error message.
- [Fix] Every phone popup can be closed: On a phone, each sheet, drawer, picker and dialog now has a close ×, closes on a tap outside, and can be swiped down from the grab handle.
App
- Added a dark public landing page at the site root, with the narrated film, a step-by-step how-to video, and sections on where your mail lives, costs and open source.
- Moved the app under /app, and old addresses (messages, backfill, usage, admin, invite links and others) redirect there. Unknown root paths now return a 404 instead of the app.
- Google sign-in now returns you to Settings ▸ Accounts inside the app, and new invite links point to the /app address.
- In Settings ▸ Email, the Private tunnel route now asks for the server's name and rejects a port or bare IP address, because the gateway picks the app by name.
- Updated the Lumen UI kit to 0.1.87:
- Times, numbers and labels no longer wrap.
- Drawer titles and subtitles truncate.
- In the Admin ▸ People drawer, Recent activity times stay on one line, and long lines end in "…" with the full text on hover.
- Phone popups gained a close ×, tap-outside to close, swipe down, Esc and back support, safe-area padding, and titles that stop short of the ×.
Sync
- Sync now opens a visible sync job, so it no longer looks like nothing happened when there's nothing pending.
- The "Running now" list shows each job, what it is doing, when it started (Pacific Time) and a live timer.
- Each sync and backfill job is logged when it ends, with its result, any error, messages copied and time taken.
- The Indexing panel and its stages are merged into one Copying section with one queue. A copied month shows "Checking" until it has been checked against Gmail's list.
- The Sync card refreshes every 5 seconds while a sync job runs and every 15 seconds while other work runs.
Storage
- The finished-job log keeps the last 30 jobs per mailbox in the existing status row.
- A finished sync job costs 2 row writes. A backfill job costs no extra writes.
- The live "what it is doing" text is kept in memory, so it costs no row writes.
Infra
- Added an optional
CF_ACCESS_TOKENsecret that lets MailCellar edit its own Access rule. If it is unset, no sync happens. The rule is edited only when the app has exactly one rule of its own, and shared rules and non-email includes such as groups are left alone. - Added an optional
SERVICE_IDENTITIESsetting that maps an Access service token's client ID to an email address, so headless agents can open pages as a named person. If it is unset, service tokens are still refused, and the token is still fully verified. ACCESS_AUDcan now be a comma-separated list, so two Access apps can work during the switch to /app.- Bound the Workers VPC service
freesendasFREESENDfor the email tunnel. - Removed the Worker CPU limit because the account is back on Workers Free, which had been blocking deploys. The setup guide explains how to add it back on Paid.
- Updated the setup guide for Access on /app and /api, plus a Bypass app for the public page. Merging alone changes no access until the Bypass app is added.
Pull requests (10)
- #143 feat: Access service token in SERVICE_IDENTITIES acts as that person (@bchoor)
- #142 feat: keep Cloudflare Access in step with the people list (@bchoor)
- #141 fix: bump Lumen to 0.1.87 so short values (times, emails) never wrap (@bchoor)
- #140 fix: bump Lumen to 0.1.86 so Send test shows the email server's own error (@bchoor)
- #139 Email: tunnel route asks for the server's name, not an IP or port (@bchoor)
- #137 Email: link MailCellar to FreeSend over the arm-01 tunnel (@bchoor)
- #138 fix: drop the Worker CPU limit, the account is back on Workers Free (@bchoor)
- #133 Backfill: show running jobs, one Copying section, and finished jobs (@bchoor)
- #136 Public landing page at /, the app moves to /app (@bchoor)
- #135 fix: bump Lumen to 0.1.77: every phone popup can be closed (@bchoor)
Full changelog: v0.0.13...v0.0.14
v0.0.13
Key takeaways
- [Feature] [Security] Choose who may use MailCellar: In the new Admin sidebar group (People, Invites, Permissions, Audit log), visible only to admins, you can now invite people by email or single-use link, give or remove their access, and review an audit log of changes.
- [Feature] Ask Claude about your mail, read-only: Claude can now connect to your mailbox through the new
/mcpendpoint to search messages, read them, open threads and attachments, and answer "how many" questions with counts grouped by month, sender, domain, recipient and more, without changing anything. - [Fix] [Security] Connecting Gmail works again: On the Accounts page, connecting or reconnecting a Gmail account no longer fails with Cloudflare Error 1101, and Google now asks only for MailCellar's own permissions (sign-in, email and Gmail).
- [Fix] New mailboxes show up on Backfill: On the Backfill page, a mailbox you just added, removed or uploaded as an mbox archive now appears right away instead of "No Gmail accounts" until you reload.
- [Fix] Month map counts are accurate: On the month map, the copied count for a month no longer shows 1–2 short, for both older months and the month new mail is arriving in.
App
- New Admin sidebar group with People, Invites, Permissions and Audit log, shown only to admins.
- Invite links open a page where the invited person accepts. If email sending isn't set up, each invite gives you a link to send by hand.
- Anyone not on the list sees "You're not on the list" instead of the app.
- Removing someone from the list keeps their mail.
- Without admin emails configured, there is no list and Cloudflare Access alone decides, so self-hosted setups are unchanged.
- MailCellar can optionally join a suite of apps: the sidebar brand becomes an app switcher and theme mode carries across the suite. With nothing configured, nothing changes.
- The Lumen UI kit is updated.
- Backfill refreshes itself whenever your list of mailboxes changes.
Gmail
- Connecting or reconnecting an account no longer crashes. The crash came from the new mbox import columns.
- Google no longer folds earlier grants (such as Drive) into MailCellar's consent screen.
Search
- New
count_messagestool answers "how many" in one count instead of paging through results. It takes the usual search filters and can group by month, year, sender, domain, label or mailbox. - Counts can be broken down a second time, for example month then domain. Any two of month, year, sender, domain, label, recipient and mailbox can be combined.
- You can now filter by recipient (
to) in both counts and searches, and group counts by recipient. - While older mail's recipients are still being read, counts say so.
- The first page of search results now includes a total.
- Searches stop reporting "more to page through" once the total is reached, and the "keep searching" note only appears when there is more.
- Tool descriptions now point Claude to counting for "how many" questions.
Sync
- Live sync now sets the copied count for each month new mail lands in from what the mailbox actually holds, so it matches Gmail's own listing.
- Six older months were recounted by re-running their index check against the whole mailbox. No mail was missing and nothing was re-copied.
- A background pass fills in recipients (To, Cc, Bcc) for mail copied earlier. It reads only message headers and stops at the daily write limit.
- New mail and mbox imports record recipients as they are copied, at no extra write cost.
Storage
- Messages now store their recipients, in lower case, for recipient searches and counts.
- A new thread index is built gradually, once the day's write budget has room.
- A read-only report shows how a mailbox database splits its bytes.
- The README now has a table of Durable Object writes per message (about 7.5) and what it means for backfills on the Free plan.
Infra
- The
/mcpendpoint has its own Cloudflare Access app with Managed OAuth. It answers 503 until its audience variable is set, and it refuses cross-site browser requests. - Setup guide: new step for the MCP Access app, plus documentation for the suite variables.
- Invite email is sent over SMTP with STARTTLS from the Worker, using the new
SMTP_HOST,SMTP_PORTandMAIL_FROMvariables and theSMTP_USERandSMTP_PASSWORDsecrets. - The new optional suite variables are
SUITE_LIST_URL,SUITE_PREFS_COOKIEandSUITE_PREFS_DOMAIN. - The people list is checked on every
/api/*and/mcprequest and cached for 60 seconds per Worker.
Pull requests (10)
- #130 Admin > People: who may use MailCellar, on Lumen's people kit (@bchoor)
- #134 feat: join the hub's app switcher and shared theme when SUITE_* is set (@bchoor)
- #132 Sync: count new mail's month against Gmail's list, not message date (@bchoor)
- #131 Accounts: fix Gmail connect crash; ask only for MailCellar's scopes (@bchoor)
- #129 MCP: two-level counts and recipients (@bchoor)
- #128 MCP: no keep-searching note once a search is done (@bchoor)
- #127 MCP: search stops paging once its total is reached (@bchoor)
- #126 MCP: count_messages and search totals (@bchoor)
- #125 MCP server: read-only /mcp for Claude (@bchoor)
- #124 Backfill: show a new mailbox without a reload (@bchoor)
Full changelog: v0.0.12...v0.0.13
v0.0.12
Key takeaways
- [Feature] Search inside email bodies: On the Messages page, searching now also matches words inside the email body, not just the subject and sender, for new mail, and older mail follows once the body pass is started by hand.
- [Feature] Attachment previews: In the message drawer's Attachments section, tapping an image, PDF or text file now opens a preview, and a new List / Grid toggle shows image thumbnails.
- [Improvement] Page buttons for groups: On the Messages page, with Group by set to Sender or Domain, groups now come in pages with previous and next buttons (such as
1–50 of 1,240) instead of a "More groups" row. - [Fix] Filter values while grouped: On the Messages page, the filter menu now lists its values (such as each Domain) when Group by is on, so you can filter a grouped view.
- [Improvement] Mailbox is the record: This affects how every mailbox stores and opens mail: each message's location is now kept in the mailbox itself, so syncs and backfills no longer wait on Basin write access and opening a message needs no Basin lookup.
App
- Attachment preview opens in a dialog over the message drawer.
- Images have previous/next buttons, ← → keys, swipe and a thumbnail strip.
- PDFs are drawn page by page.
- Text, CSV and JSON show as text.
- Any other file shows its name, size and a Download button.
- The Attachments section has a List / Grid toggle. The grid loads image thumbnails lazily.
- Group by Sender or Domain on the Messages page now pages its groups with previous/next buttons and a total count. Labels, Category and Read still show all their groups at once.
- With Group by on, the filter menu lists real values again. Previously it showed zero for Sender, Domain and the rest.
- In Panes, an empty tab (for example Labels on an imported mbox archive) keeps the tabs in place and shows "No labels in this mailbox" inside the pane. Senders, Domains and Time work the same way.
- An archive with no mail now says "Nothing is imported into yet." instead of mentioning the backfill.
- In Settings ▸ Accounts, Open Backfill on an mbox archive's row now closes Settings instead of leaving it open on top of Backfill.
- The "Waiting for Basin write access" card is gone.
Search
- Email bodies are now searchable through a body word index kept in each mailbox. The bodies themselves stay in the raw files.
- Each search word can match the subject, sender or body, so words can be split across subject and body.
- Messages that match only in the body are marked as body hits.
- A new snippets request returns the matching text and hit count for the messages on screen.
- The MCP tools get body search automatically, with no change on their side.
- Indexed text prefers the plain-text part and strips HTML only when there is no plain part. It skips attachments, cuts quoted replies, and caps at 16 KB per message.
- A body pass indexes mail that is already archived.
- It reads raw files in ID order from a saved bookmark.
- It reads each pack once, or only the first 256 KB of a large message.
- It waits for the daily write guard between runs.
- It does not start on deploy. Someone starts it by hand once approved.
Sync
- Backfill copy, the 6-hourly sync and mbox import now index each batch straight into the mailbox as they copy, behind the daily write check.
- New mail gets its body text indexed in the same step.
- Syncs and backfills no longer wait for a Basin token, and the
basin_not_readyerror is gone. - The month committer now only lands months. The path for rows staged earlier is removed.
- Live sync still drains the one old queued file so it gets indexed.
Storage
- Each mailbox now holds every message's row plus the file, offset and length of its raw bytes.
- Opening a message is one mailbox read plus one ranged read of its file. If a location is missing, it falls back to the single raw file.
- Raw files are grouped into packs: 25 messages under 128 KB, 8 up to 2 MB, and larger messages alone.
- All 122,348 copied messages were located before the Basin code was removed, and sample opens succeeded.
- The Basin code, its helper pieces and the Parquet/Iceberg packages are removed.
- Deleting Basin's tables, catalog data and token is a separate step that waits for approval.
Infra
- The project is prepared to go public under an MIT license. Repository visibility is unchanged.
- The README now credits msgvault (Wes McKinney, MIT) and describes the architecture and measured costs. It notes that the costs came from the earlier version, which wrote more to R2.
- The setup guide and example variables now need only the R2 bucket (
ARCHIVE_BUCKET). R2 Data Catalog,BASIN_TOKEN, the catalog URI and the warehouse are no longer needed. - Deploy config no longer holds personal values (account ID, route, Access or admin settings). The live Worker keeps its current variables and its custom domain.
- CI and the release dry run skip fork pull requests, so outside code never runs on the self-hosted runner.
- Attachment previews are served inline only for images (not SVG), PDFs and text. They are sandboxed with a strict Content-Security-Policy and
nosniff. Every other type stays a download. - Previews cost no mailbox reads or writes, and responses are cached privately for a day.
Pull requests (10)
- #123 Search inside email bodies: body word index, body pass, snippets (@bchoor)
- #122 README and setup guide: the mailbox is the record, no Basin (@bchoor)
- #121 Remove Basin: the mailbox DO is the record (@bchoor)
- #120 Mailbox is the record: copy indexes straight into the DO (@bchoor)
- #119 Open source prep: MIT license, README, setup guide, no personal values in config (@bchoor)
- #118 Messages: page buttons for groups (@bchoor)
- #117 Panes: keep the tabs when a tab is empty (@bchoor)
- #116 Messages: filter values with Group by on (@bchoor)
- #115 Message drawer: preview attachments (images, PDFs, text) with a grid view (@bchoor)
- #114 Open Backfill from an archive's row closes Settings (@bchoor)
Full changelog: v0.0.11...v0.0.12
v0.0.11
Key takeaways
- [Feature] Import mail from an mbox file: In Settings ▸ Accounts ▸ Add mbox archive, you can upload an mbox file from your browser and have its mail imported with no Gmail link, then follow progress on the Backfill page.
- [Feature] [Improvement] Reconnect a failed Gmail account: In Settings ▸ Accounts, an account whose Google connection has failed now shows a Reconnect button that signs you in again and keeps all the mail already copied.
- [Fix] Messages Gmail won't hand over no longer leave months stuck on "Almost": On the Backfill page, a month where Gmail lists a message but refuses to return it now counts as complete and shows a note like "1 Gmail won't give".
- [Fix] Copied counts now include new mail: On the Backfill page, the copied count for a month that is already indexed now includes mail picked up by new-mail sync, so it matches the Searchable count.
- [Improvement] Overview history by month for newer mailboxes: On the Overview page, the history chart is titled "Messages per month" and buckets by month when your mail spans under two years.
App
- Settings ▸ Accounts has a new Add mbox archive option, and a failed account now offers Reconnect instead of "remove and add again".
- Error messages for a failed account now say Reconnect.
- On the Backfill page, an mbox archive shows as Importing or Imported, with "in the file" counts per month.
- The Overview history chart switches to months when the mail spans under 24 months. Longer histories stay in years.
- The Backfill coverage map's Complete green and Almost amber are more muted in light and dark mode. The intermediate shades were lowered to stay below Complete.
Gmail
- Messages Gmail lists but answers 404 or 400 for are now recorded as unavailable instead of being dropped silently.
- Unavailable messages are subtracted from the month's expected count, so the month turns green. The map, totals, meter, presets, "All missing" and the backfill-missing action all follow this.
- Backfilling an affected month again fetches only the missing message, and the month lands marked unavailable.
- Reconnect reruns Google's consent with the account suggested. The fresh token is saved over the old one, so all copied mail stays.
- When an account stops, the message now includes Google's error code (such as
invalid_grant) and points to Settings ▸ Accounts.
Search
- The mbox import builds the search index month by month, newest first.
- Months that are already indexed now add new-mail messages to their copied count. This fixes a mailbox that showed 2,156 of 2,199 copied while Searchable showed all 2,199.
- A one-time repair fixes recent indexed months, and the status row is rebuilt once so the map and totals pick up the corrected counts.
Sync
- The 5-minute watchdog alarm now goes idle. A census that stopped on an error and the 6-hour recount start only at a sync slot, and a census held for the daily limit no longer counts as running.
- A month left showing Indexing no longer keeps the alarm on. The watchdog drops months whose index is already built.
- Right after a reconnect, the next alarm retries the census and restarts the stopped sync.
- New-mail sync adds about one extra write per month per batch to keep copied counts right. No new Gmail calls or backfill are needed.
Storage
- mbox files upload straight to raw files (R2 objects) in 64 MB parts, 3 at a time, with retries.
- An interrupted upload resumes: choose the same file again and only the missing parts go up. The only size limit is R2's 10,000 parts (about 640 GB).
- The import packs messages into raw files and appends Basin rows. Rows are staged first, so a failed append is retried.
- The upload and the packed files count on the backfill spend meter, which counts all bytes in the bucket.
Infra
- An mbox import runs as a chain of Workflow runs:
- Scan reads the file in 8 MB slices, finds messages and their months, and skips Spam and Trash.
- Copy packs the messages into raw files and appends the Basin rows.
- Index builds the search index for each month.
- Each import run makes one Durable Object call for progress, plus the call that launches the next run. The copy path makes no other reads or writes there.
- After Workers Paid ends, Free budgets apply and imports run more slowly.
- While an account is stopped, the watchdog logs one
account-errorline per alarm with the account, email and error. That is about 4 a day when idle. - The
gmail-synclog line now lists what keeps the alarm on (busy) for each account: sync, queued months, census or index. It also reports whether the alarm is watching.
Pull requests (10)
- #113 mbox archive mailboxes: upload, import and Backfill progress (@bchoor)
- #112 Count new mail as copied in months already indexed (@bchoor)
- #111 Accounts: Reconnect a failed account instead of removing it (@bchoor)
- #110 Accounts: log a broken Google connection, retry right after it's added again (@bchoor)
- #109 Overview: show messages per month when the mail spans under two years (@bchoor)
- #108 Watchdog: a month left showing Indexing no longer keeps the alarm on (@bchoor)
- #107 Watchdog: log what keeps the 5-minute alarm on (@bchoor)
- #106 Watchdog: stop the idle 5-minute alarm (@bchoor)
- #105 Backfill map: mute Complete green and Almost amber, light and dark (@bchoor)
- #104 Backfill: messages Gmail won't give count as unavailable, not missing (@bchoor)
Full changelog: v0.0.10...v0.0.11
v0.0.10
Key takeaways
- [Feature] Group and sort Messages your way: On the Messages page, use Group by to bucket mail by Sender, Domain, Labels, Category or Period, and sort by Sender, Size, Attachments or Category.
- [Feature] Pick months with two taps: On each mailbox's coverage map on the Backfill page, tap one month and then another to pick every missing month between them, using the quick-pick bar above the map.
- [Feature] Sparklines on the Backfill tiles: On the Backfill page, the Size tile now charts size copied over the last hour, and the DO writes today tile charts hourly Durable Object writes over the last 24 hours.
- [Fix] A bad message no longer fails a month: During a Backfill, a month's copy now skips a single message that Gmail refuses with an HTTP 400 error and finishes the rest of the month.
- [Fix] Missed new-mail syncs now run: The scheduled Gmail new-mail sync now runs a slot it missed after a restart, at the next watchdog check within 5 minutes.
App
- Messages has a Group by menu with Sender, Domain, Labels, Category and Period. The pager shows "N groups", and each open group loads its own pages.
- You can sort Messages by Sender and Size. Sorting by Attachments or Category is also available, and a column sorts only once its index is ready.
- A filter such as Domain now shows its total where one is available.
- Edit view greys out Pivot and shows the reason. Group stays on.
- Group by Period fixes:
- Each Period group shows its message count.
- Years now reach back to your oldest indexed month, not just 2026.
- Periods with no mail are hidden.
- "No rows match." no longer appears while grouping is on.
- Expand all and Collapse all work on grouped lists.
- Month picking on the Backfill coverage map:
- Tap a month, then another, to pick every missing month between them, even across years.
- Start on a picked month to unpick a range, or tap a year to pick all of it.
- Quick picks sit in a bar above the map: All missing, Last 3 months, Last 12 months and Clear queue. The Select months menu is gone.
- Picked months show a framed check. Months that can't be picked fade while you pick, and months already copied can't be picked.
- Nothing starts until you press Start backfill.
- The Backfill Size and DO writes today tiles now carry sparklines built from real data.
- The Size line restarts if the mailbox's Durable Object restarts, and returns once copying moves.
- The Account limits card is gone from the Backfill page. It remains unchanged on the Cloudflare usage page.
- The Plan tile now shows when your paid period ends.
- The UI kit is updated to support the grouped lists.
Gmail
- When Gmail answers 400 for a single message, MailCellar skips it, logs which message and Gmail's reason, and finishes the month.
- The month's count shows the gap left by a skipped message.
- Other Gmail fetch errors now include Gmail's own error message, so the cause is visible.
Search
- Messages can be sorted by date, sender, size, attachments or category, and grouped by sender, domain, label, category or period. Both work on the server, so they cover your whole mailbox, not just the loaded page.
- Sorting falls back to Date until the matching index exists.
- Groups are listed largest first, 50 to a page, and each group opens with its own filter.
- Sorting by Attachments lists mail with attachments first, then the rest by date. Sorting by Category reads each category by date, in name order.
- Paging was checked in both directions across 700 test messages for both new orders.
Sync
- New-mail sync now notices a slot that passed since the last saved run and runs it at the next watchdog alarm, within 5 minutes. This fixes slots lost when a deploy restarted the user's Durable Object between scheduling and the slot.
- Each Gmail sync alarm now logs its schedule decision: the schedule settings, last run, slot, whether a run was due, and the pause state. This makes a sync that fails to start easier to diagnose.
- These changes add no Durable Object writes.
Storage
- New indexes for Sender and Size (and for Attachments) are built before a month is indexed. The full backfill therefore indexes each message once.
- Indexing costs more writes per message. It was about 3.06 Durable Object rows per message before and is about 5.06 now. The Attachments index adds roughly 0.2 more.
- The index reservation per message rises from 4 to 6 rows.
- The sparkline data is kept in memory only and adds no Durable Object reads or writes.
Infra
- Paid-plan run budgets now end on their own date. The date moved to the billed period end, Oct 27 00:00 UTC, which was seven hours earlier than the earlier guess.
- Free's smaller run sizes and the 97.5k Durable Object write stop therefore return on time. Run, lane, list and census calls, CPU, index chunks, commit bytes and lane spend all follow the new date.
- The quota service re-reads your plan hourly instead of daily. A cancelled Workers subscription counts as Paid until its period ends, and that end date appears on the Plan tile and in the log.
- After the switch to Workers Free, the
limitssetting must be removed from the Wrangler config because Free can't accept it. - Spend is unchanged apart from one free billing API read an hour.
Pull requests (10)
- #103 Copy: skip and log a message Gmail answers 400 for, instead of failing the month (@bchoor)
- #101 Backfill: sparklines on the Size and DO writes tiles (@bchoor)
- #102 fix: Group by Period counts and years, Expand all; sort by Attachments and Category (@bchoor)
- #100 Paid's budgets end when billing says the period does (Oct 27 00:00 UTC) (@bchoor)
- #99 feat: Messages groups by Sender, Domain, Labels, Category and Period, and sorts by Sender and Size (@bchoor)
- #96 feat: pick months with two taps; quick picks in the bar above the map (@bchoor)
- #98 Log each Gmail sync alarm's schedule decision (@bchoor)
- #97 Backfill: drop the Account limits card (@bchoor)
- #91 feat: Sender and Size sort indexes, server sort and group endpoints (@bchoor)
- #95 Gmail sync runs a slot missed across a restart; Paid's budgets end Oct 27 on their own (@bchoor)
Full changelog: v0.0.9...v0.0.10
v0.0.9
Key takeaways
- [Feature] One Mailboxes card, opening by year: On the Backfill page, the new Mailboxes card replaces the mailbox list and the by-month map, so you can see each mailbox's status and last sync in one place and click a block to show that mailbox in the cards below.
- [Performance] Faster message opening: On the Messages page, opening a message no longer re-reads the search catalog each time, so messages from months you've already viewed should open much faster.
- [Improvement] Counts on category tabs: On the Messages page, each category tab (Primary through Forums) now shows its message count, and the pager shows the real total instead of "50+".
- [Fix] Gmail, copied and searchable counts agree: On the Backfill page, the Gmail, copied and searchable counts should now match, because live sync recounts the month new mail lands in and copied is capped on the total.
- [Performance] Bigger backfill runs on Workers Paid: This affects how fast a backfill copies and indexes mail, since each run now makes more Gmail and storage calls and saves larger batches, with a daily write guard in place of the old hard stop.
App
- The Backfill page's Mailboxes card opens by Year, with Month and Quarter chips still available.
- Each mailbox row shows a status badge and "Synced … ago", and a single percent: the map's Complete.
- The old separate percent from the sync counter is gone.
- The card also shows when you have only one mailbox.
- On a phone, the address keeps the name column and status moves to the Sync card.
- On the Messages page, the Category column is hidden on category tabs and still shown on All. Saved views keep the column.
- The Searchable caption now says what it holds beyond copied mail, for example "17,139 copied + 11 not in Gmail's count". It reads "all copied mail" when the two are equal and "N copied, not yet searchable" when search is behind. The percent is gone.
- Hovering anywhere on a stage head now shows its text, and Indexing has its own line ("Building search for N months" or "Copied mail is searchable"), where before it showed Copying's text.
- A clicked tab now shows only its underline, with no focus ring. Keyboard focus still shows the ring.
Gmail
- Gmail's count for a month is refreshed when new mail lands in it, so the October count no longer goes stale.
- Messages Gmail files under the month after their date, such as late-night UTC mail at a month's end, are no longer dropped from the copied count.
Search
- Category tab totals come from the labels rollup, and Primary is the indexed total minus the other four.
- Indexing hands over after 20 chunks instead of 4, so a month is read back from storage fewer times.
- A daily guard holds indexing until 00:05 UTC if it uses too many Durable Object writes in a day.
Sync
- Live sync lists again the current or previous month where new mail landed (a few list calls, no storage writes) and stores Gmail's count.
- Sync runs now make 400 calls, up from 45.
- The census makes 400 calls, up from 40, and month listing makes 100, up from 30.
- A lane run makes 200 outside requests, up from 40.
Storage
- The first time a month is opened, MailCellar saves where each of its messages sits as one small raw file. Later opens read that file and then only the message's byte range.
- If a message is missing from the file, for example mail copied later, the month is rebuilt once. Mail with no search-catalog entry still opens through the old single-file path.
- The Messages page asks for the locations of up to 6 months on screen in advance, once per visit, so the first click is usually warm.
- This uses no Durable Object reads or writes. The only cost is one raw-file write per month, the first time it's used.
- The committer now saves 32 MB per commit, up from 8 MB, to keep pace with about 2,500 messages a minute.
- Search-catalog rows are staged every 16 MB, so long runs never fill memory.
Infra
- MailCellar is now set up for Workers Paid, with a longer CPU limit of 120 s in the Wrangler config. Going back to Free means changing one plan setting and removing that limit.
- The $1.10 spend guard now also counts Paid overage for Workers requests and Durable Object and D1 rows, shown as overage in the spend overview. Today this adds $0, because the included amounts haven't been passed.
- Gmail pacing and the maximum number of lanes are unchanged, so Gmail's per-user rate is the cap.
- Updated the Lumen UI kit to 0.1.41 and removed the app's own tab focus-ring workaround, since the kit now handles it.
- Extended the read-only audit to report each month's census, copied, counter, index and Gmail list counts. It reads only.
Pull requests (10)
- #77 Backfill: one Mailboxes card, by year, pick a mailbox by name (@bchoor)
- #94 Spend guard: count Paid overage (Workers requests, DO and D1 rows) (@bchoor)
- #90 Messages: open a message without reading Basin each time (@bchoor)
- #93 infra: Lumen kit 0.1.41; drop the local tab focus-ring override (@bchoor)
- #92 Workers Paid: bigger runs, 32 MB commits, daily DO-write guard instead of the 97.5k stop (@bchoor)
- #89 Counts agree: recount the month new mail lands in; cap copied on the total (@bchoor)
- #88 fix: a clicked tab shows only its underline, no focus ring (@bchoor)
- #87 Audit: per-month census, counter, copied, index rows and Gmail list (@bchoor)
- #86 Category tabs: count on each tab, real total on the pager, no Category column (@bchoor)
- #84 fix: Searchable says what it holds beyond copied; Indexing has its own hover text (@bchoor)
Full changelog: v0.0.8...v0.0.9