Supported Formats
| Format | Tool | Notes |
|---|---|---|
| EML Directory | bichon-cli / WebUI | CLI: recursive directory scan preserving hierarchy; WebUI: individual .eml files (multi-select) |
| MBOX | bichon-cli / WebUI | Single-file streaming import; supports Gmail's MBOX variant |
| Thunderbird | bichon-cli | Reads directly from local Thunderbird profile directory |
| PST | bichon-cli / WebUI | Outlook Personal Storage (.pst) file parsing; folder structure preserved |
| API Import | POST /api/v1/import | Base64-encoded EML payloads for programmatic use |
| MBOX Export | bichon-cli / WebUI | Whole messages as .mbox — an entire account (CLI), a saved search, or the current result set (WebUI) |
| Attachment Export | bichon-cli / WebUI | Matching attachments packed into a .zip, deduplicated by content hash, with a manifest.jsonl |
| Export Verification | POST /api/v1/exports/{job_id}/verify | Recompute artifact hash and cross-check every message or attachment (Pro) |
WebUI limits: MBOX uploads up to 1 GB per file, PST up to 2 GB (both configurable server-side via
bichon_web_mbox_upload_limit_mb/bichon_web_pst_upload_limit_mb). EML files are capped at 100 MB. For larger archives usebichon-cli, which processes server-side with deduplication.
Importing via the WebUI
The Import page (Import EML / MBOX / PST) uploads files directly through the browser to POST /api/v1/upload-import. It walks through four steps:
1. Select target account
Only NoSync accounts are listed. Accounts that are actively syncing via IMAP are excluded — importing into a live sync pipeline would race the fetcher, so such accounts must use the CLI path or be converted. Your account needs the data:import:batch permission on the target account (administrators with system:root bypass account ACLs); otherwise the import will be rejected by the server. See Accounts for the account types.
2. Choose files
Drag & drop or browse for one or more .eml, .mbox, or .pst files. Files that exceed the size limits or have a non-email MIME type (videos, archives, PDFs, executables, …) are rejected upfront in the queue list. The server additionally checks free disk space before streaming and aborts if less than 2× the upload limit is available.
3. Folder determination
PST files carry their own folder structure (Inbox, Sent Items, …) which is preserved and created automatically during import — no folder choice is offered.
EML / MBOX files offer three modes for deciding which mail folder the messages land in:
| Mode | Behavior |
|---|---|
| Auto-detect from email headers (default) | Reads the first 64 KB of the file in-browser and picks the folder by priority: ① X-Bichon-Metadata header (a JSON object with a mailbox_name field — written by Bichon's own exports, so re-imports round-trip), ② X-Gmail-Labels header (Gmail Takeout; state labels like Unread/Opened/Archived — including localized variants in 20+ languages — are filtered out, and business labels are preferred over generic Inbox/Sent), ③ file name fallback (Inbox.mbox → Inbox). The detected folder and its source are shown as a badge before you commit. |
| Choose from existing mailboxes | Pick one of the mailboxes already present in the selected account. |
| Enter a custom folder name | Type any target folder name; it is created on import. |
4. Progress & results
The upload streams with a progress bar, then the server parses, indexes, and deduplicates each message. Live counters show success / failed / duplicates (duplicates are content-identical messages already in the archive, detected by BLAKE3 hash — they are skipped, not re-imported). Failed items are listed with per-message error messages, expandable inline.
An import history section at the bottom keeps your past runs with the same success/failed/duplicate/total breakdown per run.
Exporting
Bichon exports archived mail two ways. Both run server-side as background jobs — the browser or CLI only starts the job, polls it, and downloads the finished artifact; nothing is assembled client-side.
| Export type | Format | Contents | Source |
|---|---|---|---|
| Whole messages | .mbox | One MBOX file with every matching message | Email saved search, or an email-result filter |
| Attachments | .zip | Every matching attachment, deduplicated by content hash, plus a manifest.jsonl describing each entry | Attachment saved search, or an attachment-result filter |
MBOX requires an Email saved search and the attachment zip requires an Attachment saved search; the server rejects a mismatched pair rather than silently exporting something else. The same rule applies to ad-hoc filters: an email filter only pairs with MBOX, an attachment filter only with the zip.
Entry points
- Exports page (
/exports) — pick a saved search. The export replays that search's stored filter, so the scope is reproducible and can be run again later. - “Download all” on the search / attachments page — exports the current filter without saving a search first. The button only appears when the page narrows the result set with at least one criterion (text query, account, mailbox, tag, metadata, or date range); with no filter there would be nothing to export but the whole archive, so the button is hidden. This export is labelled Current filter in the job list and on the approval card.
Preview and job lifecycle
- Preview — the server returns the accounts, item count, and estimated size the selection covers (
POST /exports/preview). Nothing has been exported yet. - Start — creates a background job (
POST /exports). The page shows processed / exported / failed counters and supports cancel. - Download — once finished, download the artifact. A failed item (for example, an attachment whose blob is missing from storage) is counted in
failedand does not abort the rest of the job.
Where artifacts live and how long they stay
<root>/exports/<job_id>/
├── exp_xxx.mbox # or exp_xxx.zip
├── manifest.json # job summary + SHA-256 of the artifact
└── manifest.jsonl # attachment exports only: one line per zip entry- Finished artifacts are kept for 1 hour (
EXPORT_TTL_SECS). A sweep runs every 15 minutes and also removes orphaned job directories, so download before the hour is up. Deleting a job removes its directory immediately. - Completed jobs survive a server restart — they are reloaded from
manifest.json. Jobs that were still running do not: the transfer cannot be resumed, and the partial directory is reclaimed by the TTL sweep. exports/is runtime scratch space: it is not included in backups (the backup engine captures the databases, blob store, and indexes — not runtime scratch directories) and it cannot be relocated separately from the data root.
Operational limits
- Two concurrent exports. A third request is rejected until one finishes.
- Disk space. Before starting, the server checks that the export volume has the estimated size × 1.2 free and fails fast if not.
- Scope. An export is limited to accounts the caller may read and requires
data:export:batchon every account in scope. A view whose access is narrowed by an address filter cannot be used to scope a batch export — the server refuses rather than widening it to the whole source account. - Dual control. On Enterprise,
export.batchcan be placed under dual control. Starting an export then creates a pending approval instead of a job (the API answers202), and the export runs only after a second person approves. The approval records the exact selection — saved search id, or the ad-hoc filter frozen at request time — so what runs is what was reviewed.
Download tickets
GET /api/v1/exports/{job_id}/download requires the Bearer token. For browser downloads use POST /api/v1/exports/{job_id}/download-ticket: it returns a URL that is single-use and valid for 60 seconds, so large artifacts stream without putting the token into a navigation URL.
Verification (Pro)
Pro can recompute the artifact's SHA-256 and cross-check every entry against the live archive — messages for an MBOX export, attachment content hashes for a zip export. Results are persisted and survive a restart.
API
POST /exports/preview and POST /exports take the same body. Provide exactly one of:
saved_search_id— replay a saved search; orfilter— an ad-hoc selection, shaped{"email": {…}}for MBOX or{"attachment": {…}}for a zip. Addlabelto name the export in the job list.
format is "Mbox" (default) or "Attachments".
| Method | Path | Description |
|---|---|---|
POST | /api/v1/exports/preview | Preview the selection's scope (accounts, count, size) |
POST | /api/v1/exports | Start an export job (202 if dual control queues it) |
GET | /api/v1/exports | List your export jobs |
GET | /api/v1/exports/{job_id} | Job status and counters |
POST | /api/v1/exports/{job_id}/cancel | Cancel a running job |
DELETE | /api/v1/exports/{job_id} | Delete a job and its artifact |
GET | /api/v1/exports/{job_id}/download | Download the artifact (MBOX or ZIP) |
POST | /api/v1/exports/{job_id}/download-ticket | One-time, 60-second download URL |
POST | /api/v1/exports/{job_id}/verify | Verify the artifact and every entry (Pro) |
Processing Pipeline
All imports flow through the Bichon REST API. The server:
- Parses MIME structure
- Extracts metadata
- Indexes content into Tantivy
- Deduplicates by BLAKE3 content hash
- Stores raw blobs in bichon-blob
If the target account has an archive filter, messages the filter rejects are not stored. They still count as successful imports — the file was processed; there is simply no message in the archive.