Bichon stores an archived message as two things: an envelope (the metadata row that search, filtering and the message list operate on) and one or more blobs (the raw bytes — the message body, plus each attachment, content-addressed and deduplicated).
If a blob goes missing while its envelope survives, the message still shows up in search results and in the list — but it cannot be opened. Message repair fixes exactly that case: it re-fetches the message from its source IMAP server and restores the missing blobs in place. The envelope, its id, its folder placement and its index entry are all kept, so a repair never produces a duplicate.
What it touches
| Restored | Missing content blobs — the message body, and any missing attachment |
| Untouched | The envelope row, its id, its folder, its Tantivy index entry |
| Not repairable | A blob that is present but corrupt — see below |
Not to be confused with
Two other mechanisms fix different kinds of damage, and all three are sometimes called "repair":
- Gap-fill — diffs each remote folder against the archive and re-downloads messages that were never archived at all. It fixes missing messages. Archive-filtered candidates are re-checked from their headers and skipped without downloading their bodies.
- Full re-sync — a
UID SEARCH ALLpass over a mailbox, for when an incremental download was interrupted. Also message-level.
Neither of them can help with a message you already have but cannot open. That is what this page is about.
How Bichon decides something is broken
The check is an integrity verdict, not a presence check. Bichon re-attaches the message's stored blobs into the stripped EML (attachments are stored detached, with placeholders left in their place) and recomputes the content hash. The message counts as broken only when that recomputation does not match the hash recorded on the envelope.
Two preconditions are checked first, and both fail fast:
- The envelope must exist — otherwise
404 Not Found. - The envelope's recorded
attachment_countmust agree with the number of attachment blobs. If it does not, the repair is reported asfailedwithout contacting IMAP at all.
How it recovers the content
From the source IMAP server, in order:
- Fetch by the stored UID.
- If that misses,
UID SEARCH HEADER Message-IDover a bounded candidate set — at most 32 candidates, so a forged or duplicatedMessage-IDcannot turn a repair into an unbounded scan. - A fetched body is accepted only if its computed content hash matches the envelope's. A mismatch means the server is returning different content, and it is discarded rather than stored.
Recovered bytes go through the same first-write-wins blob store as normal ingestion, which is what makes repeating a repair safe: an existing key is never overwritten, and the store never creates a second copy of content it already holds.
Outcomes
Every repair returns one of four statuses:
| Status | Meaning |
|---|---|
complete | The message was not broken — nothing was written |
repaired | Blobs were missing and are now restored |
partial | Something is still missing after the attempt |
failed | The envelope was unusable, or the IMAP fetch/parse failed — nothing was written |
A failed repair still returns HTTP 200. The status is a verdict about the record, not a transport error — a caller that retries on a non-2xx response would otherwise retry a case that cannot succeed.
Each report lists the missing objects before and after the attempt, so you can see precisely what changed.
Corrupt blobs are reported, not overwritten
A blob that exists but whose content no longer matches is deliberately left alone. Repairing it would mean deleting the existing object first, and Bichon does not do that as part of a repair — the blob store's no-overwrite rule is what makes every other write path idempotent. Such a message reports partial on every attempt rather than being silently rewritten.
Interrupted backups
Blob writes are refused while a backup capture window is open. If that happens during a repair, the recovery still runs but the write is skipped: the report comes back partial with skipped_due_to_backup set. Re-run the repair once the backup finishes.
Self-healing on read
Repair is not the only way a missing blob is recovered. Reading a message whose content blob is absent runs the same recovery inline, so a message that is merely unread-after-a-disk-event often opens normally without anyone asking for a repair. The difference is that self-healing produces no report and no history entry — it just returns the bytes. The explicit repair endpoint is what gives you a status, a record, and an audit trail.
Triggering a repair
In the WebUI the action appears in the row menu on search results, on attachment rows, and — on Pro — on the integrity check failures table, which is the most convenient starting point because it lists exactly the messages whose blobs went missing. All three open the same dialog, which shows the before/after report. A Repair history button in the search toolbar opens the most recent reports.
| Method | Path | Description |
|---|---|---|
POST | /api/v1/repair-message/{account_id}/{envelope_id} | Repair one message |
GET | /api/v1/repair-reports | Recent repair reports (limit, account_id); default 50, capped at 200 |
Repair requires the data:manage permission on the account. The history endpoint returns only records for accounts the caller holds that permission on.
Both endpoints are available in all editions — repairing is a core data-recovery operation, not a Pro feature. What is Pro is the integrity check that surfaces which messages need it in the first place, and the audit/SIEM categorization of repairs.