Skip to content
clee704Public

About

File-based email backup and search tool. Own your mail forever.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ownmail

Your email archive, with a web interface for desktop and mobile.

Keep your mail as standard .eml files on your own drive. Search across accounts, browse labels, read HTML messages, and preview attachments in your browser. Start or schedule downloads in the web interface. The command line also handles downloads, imports, and archive maintenance.

The goal is to own your mail and remove server copies once they are no longer needed by your mail clients. Read Owning your mail for the product philosophy, Active ownership, and server-cleanup limits.

Development version: This README describes 0.4.0-dev on master. PyPI currently provides 0.3.0, which has an older interface. Use the GitHub installation below for the features described here.

Browse your archive

  • Search and navigate: Full-text search, sender links, labels, attachment indicators, sorting, and pagination across your saved mail.
  • Read on desktop or phone: A collapsible sidebar becomes a navigation drawer on smaller screens. Add the site to your phone's Home Screen for app-style access.
  • Read messages and attachments: View sanitized HTML, expand message details, print a message, and preview supported attachment formats or download the originals.
  • Choose your appearance: Light, dark, or system theme, with adjustable desktop message and sidebar density, dates, time zone, and page size. In Settings, choose Comfortable, Standard (the default), or Compact spacing; density is saved automatically in your browser.
  • Control remote content: Load remote images, fonts, audio, and video for a message or remember a trusted sender. Blocking is enabled by default.
  • Manage saved mail: Select messages, move them to the archive's Trash, and restore them. Mobile selection supports a long press.

Your .eml files and JSON label sidecars preserve the archive; SQLite provides its rebuildable search index. Downloads are incremental and resumable with Ctrl-C. You can also import .eml exports from other mail tools.

For message bodies labelled Latin-1, Ownmail prefers UTF-8 when the complete payload decodes as readable UTF-8; otherwise it retains Latin-1. Other usable charset declarations keep their precedence. Undeclared bodies use best-effort recovery, including short Korean text and text after long HTML preambles.

Install

Development version from GitHub

Requires Python 3.10+, Git, and pipx. For the web interface, also install a supported Node.js LTS release with npm. Connecting a mail provider requires a working system keychain.

pipx install "git+https://github.com/clee704/ownmail.git@master"

master follows ongoing development and can change between installations. To switch an existing pipx installation from PyPI to this version:

pipx install --force "git+https://github.com/clee704/ownmail.git@master"

To refresh an installation made from GitHub:

pipx reinstall ownmail

Reinstallation uses the original source and resolves dependencies again. It also picks up new commits when the development version number has not changed. After installing or reinstalling, run ownmail setup --web before ownmail serve so the web interface's sanitizer dependencies match the new version. For a source checkout or editable installation, see Contributing.

Published version from PyPI

To use the published 0.3.0 release with its web dependencies:

pipx install "ownmail[web]==0.3.0"

Follow the 0.3.0 README for that version's behavior and requirements.

Quick start

Run these commands from a directory where you want to keep config.yaml:

# Connect a mail provider and choose where to store the archive
ownmail setup

# Download eligible mail
ownmail download

# Open the archive in your browser
ownmail serve

Setup creates the configuration and stores credentials in your system keychain. Choose IMAP for a provider that accepts an app password, or Gmail API with OAuth for read-only Gmail access. See provider setup and configuration. Setup then installs the HTML sanitizer's Node.js dependencies for the web interface, which needs npm and internet access.

The browser opens at http://127.0.0.1:8080. ownmail serve never installs packages. If the sanitizer dependencies are missing or outside the versions this ownmail requires, for example after an upgrade, the server refuses to start and asks you to run ownmail setup --web. That command installs or refreshes only those dependencies, without credential prompts. With them installed, local reading and search work without internet; keep ownmail serve running while you browse. A phone also needs a connection to that server. See browser and mobile access.

Download from the browser

Open Settings → Downloads and choose Download now to run the same download as ownmail download, using all configured sources and their existing filters. The page shows the current source, archived and Active-refreshed message counts, and failures with a brief reason. Counts cover the current run across all sources; incomplete Active refreshes remain marked incomplete when capture finishes. Command-line and web downloads cannot overlap for the same archive.

For automatic downloads, choose an interval and Save schedule. Available intervals are 15 minutes, 30 minutes, 1 hour, 6 hours, and 1 day; Off disables the schedule. The default is Off, and the saved interval survives server restarts. Each interval starts after a download finishes or the server restarts. Keep ownmail serve running for scheduled downloads; the browser can be closed. The interval is stored as web.download_interval_minutes in config.yaml.

What gets archived

Downloads use the original incremental archiver by default. Inbox, Drafts, Trash, and Spam are excluded by the default download filters.

Active mail is opt-in. Set active_downloads: true on a source in config.yaml to cache Inbox, Drafts, and observed unfinished mail for reading and search. Active copies remain server-owned. Filed and Sent mail is archived after a fresh eligibility check; unknown states remain uncaptured.

Remove that setting or set it to false to restore the original incremental download path. Existing Active copies stay readable with a disabled/stale status; downloads do not open or update their cache. Owned mail is preserved.

Opted-in sources use targeted Active checks and incremental capture by default; ordinary retained Archive mail needs no manual exclusion. Optional active_exclude_folders: [Archive] (IMAP) or active_exclude_labels: [Saved] (Gmail API) also suppresses Active tracking for matching messages. New eligible mail in those folders or labels still gets archived. See download filters and provider limits.

Server cleanup

Preview which archived messages could be moved to server Trash:

ownmail cleanup --source personal

Cleanup verifies the owned contents and capture metadata, the Gmail account, and current message and thread activity. It reports held messages and failures with reasons. Local contents and labels stay unchanged; IMAP cleanup remains held.

To enable apply for an existing Gmail API source, explicitly authorize cleanup:

ownmail authorize-cleanup --source personal

This opens Google consent for gmail.modify, a broad permission that allows reading, changing, and sending mail. Ownmail uses cleanup access to verify and move eligible server copies to Trash. Its credential is stored separately from the read-only login used for downloads and previews.

Then explicitly request Trash moves:

ownmail cleanup --source personal --apply

Apply uses the saved cleanup credential and never opens consent automatically. See cleanup checks, authorization, retry behavior, and provider limits.

Search

Use the search box in the web interface or the same query syntax in the terminal:

ownmail search "invoice"
ownmail search "from:example.com"
ownmail search "subject:receipt"
ownmail search "attachment:pdf"
ownmail search 'label:"Receipts, 2026"'

The web interface includes search help with supported operators and examples.

For scripts, JSON mode searches the existing archive and cached Active view:

ownmail search 'invoice' --json --limit 50 --offset 0
ownmail search --json -- '-is:active'

Stdout is one JSON document; configuration and search diagnostics go to stderr. Success contains results and pagination. Each result has id, sender, subject, date, snippet, has_attachments, archived, and active. Indexed headers can be null. Snippets are capped at 200 Unicode characters; file paths and full bodies are omitted. active is null for archive-only messages, otherwise it contains complete, checked_at, content_at, reason, and refresh_error. An archived copy can also have Active status; incomplete freshness stays explicit.

--limit defaults to 50 and accepts 1–500. JSON-only --offset accepts integers from 0 to 2147483647. Pagination repeats these values and reports has_more plus next_offset (null on the last page), using one lookahead row rather than an expensive total. Ordering and Trash/date exclusions match ordinary search. Each invocation takes its own read snapshots, so later pages can change if mail changes. Empty results succeed with exit status 0. Query/FTS errors, bad pagination, missing prerequisites, and unexpected failures return {"error":{"code":"...","message":"..."}} with codes invalid_query, invalid_pagination, unavailable, or search_failed and exit status 1; Ctrl-C returns search_failed and status 130. CLI syntax/help handling retains the normal argparse behavior.

JSON search requires existing compatible indexes. It opens live SQLite data read-only, including committed WAL contents; normal WAL/SHM coordination files may be updated. It does not initialize or migrate databases, rebuild the Active index, modify substantive archive/cache/configuration/credential contents, or contact mail servers. A missing or inconsistent index reports unavailable instead of repairing it. Concurrent Active changes also report unavailable so the command can be retried. The human-readable default is unchanged.

Read a search result

Use the exact id from JSON search to read its locally available message:

ownmail read 'MESSAGE_ID'
ownmail read 'MESSAGE_ID' --body-limit 5000

The command always emits one JSON document. Success includes id, decoded sender, subject, combined recipients (To/Cc/Bcc), date, body, attachment_names, archived, and active. Ownership and Active freshness use the same fields as search; an owned copy can also have an Active server copy. Reading an ID resolves that local copy directly, even if it no longer matches the earlier query. Missing or changed cached content reports an error; no server fetch is attempted. Each command takes a new snapshot.

Body text prefers plain text and otherwise extracts text from HTML without loading images or other remote content. Attachment contents, including attached messages, are excluded; names remain separate list items even when they contain commas. Valid declared UTF-8 is decoded strictly, preserving all Unicode text; other declarations and malformed encodings use the existing recovery helpers. The body defaults to at most 20000 Unicode characters; --body-limit accepts 1–100000. body_limit repeats the requested cap and truncated explicitly says whether body text was omitted. The cap applies to body text, not headers or the attachment-name list. Full message bytes must be locally readable to parse MIME.

original contains the selected message's local file: URI and media type message/rfc822, so it can be opened in a mail client. This explicit reference includes its local archive/cache path; cached files may disappear after refresh. No attachment content or HTML is returned. Existing compatible indexes are required under the same live read-only contract as JSON search: normal SQLite WAL/SHM coordination writes are permitted, but substantive data is unchanged and no initialization, migration, cache rebuild or credential access occurs.

Success exits 0. Errors use the same error.code/error.message envelope and exit 1: invalid_id for an empty ID, invalid_limit for a bad body cap, not_found for an unknown ID, unavailable for missing/unreadable files or prerequisites, parse_failed when MIME parsing fails, and read_failed for an unexpected failure. Ctrl-C returns read_failed and exit 130. Diagnostics go to stderr; CLI syntax/help retains normal argparse behavior.

Export

Export owned archive messages to a new mbox file that other mail clients can import:

ownmail export --format mbox --output backup.mbox
ownmail export --output receipts.mbox --query 'from:billing@example.com after:2024-01-01'
ownmail export --output filed.mbox --query='-is:active'
ownmail export --format pdf --output receipt-pdfs --query 'from:billing@example.com'

Export uses the existing search syntax without a result-count limit. As in ordinary archive search, Trash and messages without parsed dates are excluded. Use scan to register untracked mail and rebuild to repair indexed dates first. Active-only cached messages are not exported. Ownership filters such as is:active can select archived copies with verified existing Active membership, but only the archived bytes and labels are used. Export never downloads mail.

The default mbox format retains original headers, MIME bodies and attachments, with standard mbox envelope lines and From escaping. Sidecar labels are not added as headers.

PDF exports create a new directory containing 000001.pdf, 000002.pdf, and so on, in date-ascending message order. Each document prints the subject, sender, recipients, date, plain-text or sanitized HTML body, and an attachment inventory on A4 pages; wide HTML is scaled to fit the page width. A message's own page setup, such as a page size or running header, is ignored. Attachment payloads are not embedded in the PDF; safe inline CID images remain visible. Remote content and local-file loads are blocked. An empty selection creates an empty directory.

PDF is optional and requires available Node.js, the already-declared Playwright package, and Chromium. These are checked only when PDF is selected. If unavailable, the command fails clearly before creating output. Ownmail never installs or downloads renderer tooling, and mbox export does not require it.

The output parent directory must exist. Existing files, directories and symlinks are refused. Unreadable message files are skipped and reported; the output contains the readable matches, and the command exits unsuccessfully to flag the partial export. Handled renderer/write errors or Ctrl-C remove the new incomplete export so the same command can be rerun. Output can be visible while being published; abrupt process termination can leave partial output. Source mail is never changed.

Commands

Command Purpose
setup Connect a mail source, store credentials, and install web interface dependencies; --web installs only the dependencies
download Refresh Active mail and archive eligible filed and Sent mail
cleanup --source NAME Preview server cleanup; --apply requests eligible Gmail Trash moves
authorize-cleanup --source NAME Grant separate Gmail cleanup access through explicit browser consent
serve Open the web interface
search "query" Search from the terminal
export --output PATH Export matching archived messages as mbox or PDF
import <path> Import external .eml files
scan Index .eml files already in the archive
stats Show archive statistics
verify Check hashes, files, and index integrity
sync-check Compare the archive with its mail sources
trash View and manage the archive's Trash
update-labels Backfill missing labels
relabel Repair IMAP folder labels from the server
reconcile Review archived mail against legacy role and folder filters
rebuild Rebuild the index or selected metadata
reset-sync Reset download progress for a rescan
list-unknown List messages with unparseable dates
sources list List configured mail sources

Run ownmail <command> --help for options. See archive storage, verification, and repairs for maintenance.

download ends with an overall summary of downloaded emails, errors, and the current archive size. With --source, download and error counts cover that source; the archived total still covers the whole archive.

Privacy and security

Credentials are stored in the system keychain. Messages, attachments, label sidecars, the search index, and account configuration remain on your filesystem and can contain sensitive information. Use encrypted storage and protect your backups; ownmail does not encrypt the archive itself.

The web interface sanitizes message HTML with DOMPurify. Form controls in a message are displayed but cannot submit anything. Remote content blocking is enabled by default. It stops every request a message makes for remote images, fonts, audio, and video, including CSS and responsive images and fonts from services such as Google Fonts. Loading remote content explicitly can reveal your request to the servers that host it.

To print a message, choose Print from the reader's message actions menu. It opens the message alone in a new tab, laid out like a PDF export with the reader's remote-content setting, and opens the print dialog, which sets the paper size. Cmd+P (Ctrl+P on Windows and Linux) in the reader does the same. Printing the reader through the browser's menu prints the message details and, for plain-text messages, the body; for HTML messages it prints a note that points to the Print action, because browsers do not split the reader's message frame across pages.

The server listens on localhost by default and has no built-in login. If you make it reachable from other devices, control access through a trusted network or an authenticated proxy, and list the names and addresses browsers use in web.allowed_hosts; requests addressed to any other host get 400 Bad Request. Home Screen access uses the same server; it does not store an offline archive on the phone.

Changes made through the web interface, such as trash, labels, and settings, need an allowed origin and a CSRF token from a page the server rendered, so another website cannot make them through your browser. Requests without both, including scripts and other clients without a browser, get 403 Forbidden. Behind an HTTPS proxy, list the browser's origin in web.allowed_origins.

The Home Screen manifest uses the configured app name, root navigation scope, app icons, and the existing theme colors. A loaded reader saves only its local URL, message-file version, and vertical scroll offset in session storage. A reload restores that position only for the same unchanged message; browser Back/Forward restoration and new user movement take precedence. No message body or attachment is cached for offline reading.

While an app page is open, navigation and message-action requests stop showing pending feedback after 30 seconds. Retry opens the requested page again, or reloads the current page after an unconfirmed action. Check its current state before repeating an action: a lost response does not prove the server did nothing. Retry never automatically repeats a mutation. A cold launch while the server is unavailable still uses the browser's connection-error screen.

A future cached offline fallback requires a trusted HTTPS origin for phone/LAN access (the localhost secure-context exception does not cover a LAN address). Its intended scope is static app-shell/reconnection guidance, not private messages, search results, or attachments. No service worker or offline cache is installed yet. Actual iPhone Home Screen, keyboard, cutout, and background/resume verification remains tracked in TASK-50; desktop browser emulation does not complete those checks.

Contributing

See CONTRIBUTING.md for development setup and repository rules, and AGENTS.md for AI-agent instructions. Planned and in-flight work lives in the backlog.

License

MIT

About

File-based email backup and search tool. Own your mail forever.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages