{"id":50874767,"url":"https://github.com/ufukayyildiz/omnidock","last_synced_at":"2026-06-15T08:33:58.410Z","repository":{"id":362818661,"uuid":"1260819126","full_name":"ufukayyildiz/omnidock","owner":"ufukayyildiz","description":"OmniDock is an open-source Cloudflare email dashboard for teams that want a private support inbox, multi-domain email routing, Cloudflare Email Sending, Cloudflare Email Routing, R2 file management, contacts, signatures, and external inbox sync in one Workers app.","archived":false,"fork":false,"pushed_at":"2026-06-14T21:32:48.000Z","size":580,"stargazers_count":2,"open_issues_count":3,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-14T22:08:41.986Z","etag":null,"topics":["cloudflare","cloudflare-api","cloudflare-d1","cloudflare-email","cloudflare-email-routing","cloudflare-email-workers","cloudflare-r2","cloudflare-workers","email-automation","email-sender","r2"],"latest_commit_sha":null,"homepage":"https://omnidock.org","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ufukayyildiz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-05T22:58:13.000Z","updated_at":"2026-06-14T20:23:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ufukayyildiz/omnidock","commit_stats":null,"previous_names":["ufukayyildiz/emailfox","ufukayyildiz/omnidock"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ufukayyildiz/omnidock","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ufukayyildiz%2Fomnidock","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ufukayyildiz%2Fomnidock/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ufukayyildiz%2Fomnidock/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ufukayyildiz%2Fomnidock/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ufukayyildiz","download_url":"https://codeload.github.com/ufukayyildiz/omnidock/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ufukayyildiz%2Fomnidock/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34355157,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-15T02:00:07.085Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare","cloudflare-api","cloudflare-d1","cloudflare-email","cloudflare-email-routing","cloudflare-email-workers","cloudflare-r2","cloudflare-workers","email-automation","email-sender","r2"],"created_at":"2026-06-15T08:33:57.731Z","updated_at":"2026-06-15T08:33:58.396Z","avatar_url":"https://github.com/ufukayyildiz.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# OmniDock\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eSelf-hosted email operations for Cloudflare Workers, Email Routing, Email Sending, D1, and R2.\u003c/strong\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://omnidock.org\"\u003eWebsite\u003c/a\u003e\n  ·\n  \u003ca href=\"#quick-start\"\u003eQuick start\u003c/a\u003e\n  ·\n  \u003ca href=\"#features\"\u003eFeatures\u003c/a\u003e\n  ·\n  \u003ca href=\"#security-notes\"\u003eSecurity\u003c/a\u003e\n  ·\n  \u003ca href=\"docs/GITHUB_SEO.md\"\u003eGitHub SEO checklist\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg alt=\"Cloudflare Workers\" src=\"https://img.shields.io/badge/Cloudflare-Workers-F38020?logo=cloudflare\u0026logoColor=white\"\u003e\n  \u003cimg alt=\"Cloudflare D1\" src=\"https://img.shields.io/badge/Cloudflare-D1-F38020?logo=cloudflare\u0026logoColor=white\"\u003e\n  \u003cimg alt=\"Cloudflare R2\" src=\"https://img.shields.io/badge/Cloudflare-R2-F38020?logo=cloudflare\u0026logoColor=white\"\u003e\n  \u003cimg alt=\"React\" src=\"https://img.shields.io/badge/React-19-61DAFB?logo=react\u0026logoColor=111827\"\u003e\n  \u003cimg alt=\"TypeScript\" src=\"https://img.shields.io/badge/TypeScript-6-3178C6?logo=typescript\u0026logoColor=white\"\u003e\n  \u003cimg alt=\"License MIT\" src=\"https://img.shields.io/badge/license-MIT-16a34a\"\u003e\n\u003c/p\u003e\n\nOmniDock is an open-source Cloudflare email dashboard for teams that want a private support inbox, multi-domain email routing, Cloudflare Email Sending, Cloudflare Email Routing, R2 file management, contacts, signatures, and external inbox sync in one Workers app.\n\nThink of it as a compact Linux-style command center for domain email: not a hosted mailbox provider, not a SaaS lock-in, and not a black box. You fork it, connect your own Cloudflare account, keep your own D1/R2 data, and run the dashboard on your own Worker.\n\nWebsite: [omnidock.org](https://omnidock.org)\n\n## Why OmniDock\n\nCloudflare gives developers strong primitives for email and storage, but the operational workflow is split across zones, Email Routing rules, Email Sending checks, D1 tables, R2 buckets, external inboxes, and manual dashboard work. OmniDock brings those pieces into one focused admin UI.\n\nUse OmniDock when you need:\n\n- A self-hosted Cloudflare support inbox for `support@`, `info@`, `billing@`, or project addresses.\n- Multi-domain email routing across several Cloudflare zones.\n- A private email dashboard for agencies, internal teams, SaaS side projects, or product support.\n- Cloudflare Email Routing and Email Sending automation without building your own admin panel.\n- R2 attachment storage and a lightweight R2 bucket manager next to email.\n- Gmail, Outlook, Yahoo, iCloud, or custom IMAP/SMTP profiles pulled into the same workspace.\n- A public, fork-first repository that avoids committing account ids, D1 ids, bucket names, tokens, passwords, or personal domains.\n\nOmniDock is not an IMAP/POP3 server and does not replace a full hosted mailbox provider. It is best for private support inboxes, project inboxes, catch-all workflows, domain operations, and lightweight email management that already lives on Cloudflare.\n\n## Features\n\n| Area | What OmniDock provides |\n| --- | --- |\n| Inbound email | Cloudflare Worker `email()` handler, Email Routing support, mailbox rules, catch-all routing |\n| Outbound email | Cloudflare Email Sending plus external SMTP sending for configured accounts |\n| Storage | D1 metadata, R2 raw messages, R2 attachments, R2 manual files, extra R2 buckets |\n| Inbox workflow | Inbox, sent, archive, delete, read state, thread view, mailbox scope, global search |\n| Compose | Rich text, links, colors, signatures, attachments, attachment loading guards |\n| Domains and rules | Cloudflare zone sync, sending/routing status, default domain, mailbox routing rules |\n| Contacts | Manual contacts, CSV/TXT/VCF import, phone, company, tags, notes, edit/delete |\n| Signatures | Mailbox-based rich signatures with text styling and links |\n| External accounts | Gmail and custom IMAP/SMTP profiles with Worker-secret credential references |\n| Buckets | Browse R2 folders, preview PDF/image/text files, upload, download, delete, search paths, searchable PDFs, and saved OCR/text indexes |\n| Logs | Audit log table for sync, sending, errors, warnings, exports, and cleanup |\n| UI | Linux, Ubuntu, Fedora, Plasma, and Graphite palettes with compact desktop layout |\n\n## What It Does\n\n- Receive inbound mail through a Cloudflare Worker `email()` handler.\n- Store message and thread metadata in Cloudflare D1.\n- Store raw MIME messages, attachments, and manual files in Cloudflare R2.\n- Send replies and outbound messages through Cloudflare Email Sending or configured external SMTP accounts.\n- Sync Cloudflare zones, Email Sending status, Email Routing status, catch-all state, mailbox routing rules, and external IMAP inboxes from one button.\n- Continue long external inbox pulls in D1-backed jobs so page refresh does not cancel the sync.\n- Manage multiple domains and mailbox addresses from one dashboard.\n- Route one mailbox address or all unmatched domain mail with catch-all.\n- Search inbox, sent, and archive across subject, body, sender, and recipient.\n- Search R2 object paths, supported text files, searchable PDFs, and saved text indexes.\n- Archive, unarchive, and delete threads.\n- Compose rich email with bold, italic, underline, text color, background color, links, signatures, and attachments.\n- Preview PDF, image, text, and supported attachment files before download.\n- Import contacts manually or from CSV, TXT, and VCF files; edit contacts one by one; store phone, company, tags, and notes.\n- Manage mailbox-specific rich signatures with text style and links.\n- Add external account profiles for Gmail, Outlook, Yahoo, iCloud, or custom IMAP/SMTP settings. OmniDock stores the Worker secret name, not the credential value.\n- Browse one or more configured R2 buckets from the sidebar, create folders, preview PDF/image/text objects, upload files with progress, download files, delete files, and run a D1-backed Index Engine for searchable text and OCR-style document extraction.\n- Review app activity in Logs, export logs, and delete logs from D1.\n- Choose between five UI palettes: Linux, Ubuntu, Fedora, Plasma, and Graphite.\n- Set a default mailbox and customize automatic refresh timing.\n\n## Screenshots\n\nThe default Linux palette is compact and terminal-like, with mailbox selection, inbox/sent/archive folders, Cloudflare sync, buckets, and compose controls on one screen.\n\n![OmniDock Linux inbox](docs/screenshots/omnidock-inbox-linux.png)\n\nDomain routing, catch-all, mailbox rules, contacts, external accounts, signatures, and refresh settings live under Settings so the inbox stays focused.\n\n![OmniDock rules and domain settings](docs/screenshots/omnidock-rules-linux.png)\n\n## Quick Start\n\nShort version for a clean Cloudflare Git deployment:\n\n1. Fork this repository.\n2. Create one Cloudflare D1 database and one Cloudflare R2 bucket.\n3. Create a Worker from Git and select your fork.\n4. Set the build command to `npm run build`.\n5. Set the deploy command to `node tools/deploy-preserving-bindings.mjs`.\n6. Add build variables for `OMNIDOCK_D1_DATABASE_ID` and `OMNIDOCK_R2_BUCKET_NAME`.\n7. Add runtime values for `ADMIN_PASSWORD`, `PRIMARY_DOMAIN`, and `CLOUDFLARE_API_TOKEN`.\n8. Open the Worker URL, finish setup, run Sync, and create mailbox rules.\n\nThe fork-first flow is intentional. It keeps your install private, keeps your Cloudflare resource ids out of upstream source control, and prevents Git updates from breaking D1/R2 bindings.\n\n## Why Fork First\n\nDo not deploy OmniDock directly from the upstream repository. Fork it first, then deploy your own fork.\n\nThat gives you:\n\n- A repository you control.\n- A clean place to keep your own deployment settings.\n- Safer future updates.\n- No one-click deploy magic that hides Cloudflare bindings from you.\n\nRecommended install flow:\n\n1. Fork this repository.\n2. Open Cloudflare Workers \u0026 Pages.\n3. Create a D1 database and an R2 bucket.\n4. Create a Worker from Git and select your fork.\n5. Add the build variables listed below in `Settings \u003e Build \u003e Build configuration`.\n6. Set the deploy command to the binding-safe command listed below.\n7. Add runtime variables and secrets in Worker settings.\n8. Open the Worker URL and finish setup inside OmniDock.\n\n## Critical Binding Rule\n\nCloudflare Wrangler treats the deploy config as the source of truth. If you add D1 or R2 only in the dashboard and then deploy from Git with a config that does not contain those bindings, Wrangler can remove them.\n\nOmniDock avoids that by generating `DB` and `MAIL_BUCKET` into the deploy config from build variables:\n\n- `OMNIDOCK_D1_DATABASE_ID`\n- `OMNIDOCK_R2_BUCKET_NAME`\n- `OMNIDOCK_EXTRA_R2_BUCKETS` for any additional R2 bucket names\n\nThe default Worker script name is `omnidock`. If your deployed Worker uses a different script name, add `WORKER_SCRIPT_NAME` as a build variable with that exact name before deploying.\n\nUse these Cloudflare Workers Builds commands:\n\n| Cloudflare field | Recommended value |\n| --- | --- |\n| Build command | `npm run build` |\n| Deploy command | `node tools/deploy-preserving-bindings.mjs` |\n\nAlternative: leave Build command empty and set Deploy command to:\n\n```bash\nnpm run deploy\n```\n\nDo not use a bare deploy command of `npx wrangler deploy` for normal Git updates.\nThat command cannot safely reconstruct dashboard-only resource bindings. Use the OmniDock deploy script so existing D1/R2 bindings are read, merged, and carried forward.\n\n## 0. Prepare Cloudflare\n\nBefore deploying, prepare these items.\n\n### Cloudflare Account\n\nYou need a Cloudflare account with Workers enabled. Production email routing also requires at least one active Cloudflare-managed domain.\n\n### Domain\n\nAdd your email domain to Cloudflare and make sure the zone is active.\n\nExamples:\n\n- `example.com`\n- `company.com`\n- `support.example.com`\n\n### Email Sending\n\nEnable Cloudflare Email Sending for every domain or subdomain you want to send from.\n\nOmniDock can send only from mailbox addresses that exist in D1 and belong to a Cloudflare-verified sending domain.\n\n### Email Routing\n\nEnable Cloudflare Email Routing for every domain that should receive mail.\n\nIn OmniDock you can choose one of two routing styles:\n\n- Mailbox rule: route a single address such as `support@example.com` to the Worker.\n- Catch-all: route all unmatched addresses for the domain to the Worker.\n\nMailbox rules are safer for most setups. Catch-all is powerful, but it also receives misspelled and unknown addresses.\n\n### D1 And R2\n\nCreate:\n\n- One D1 database for metadata.\n- One R2 bucket for raw messages, attachments, and manual files.\n\nSuggested names:\n\n```bash\nomnidock-db\nomnidock-mail\n```\n\nThe actual D1 `database_id` and R2 bucket name must be added as build variables so updates do not disconnect bindings.\n\n### Cloudflare Automation Token\n\nOmniDock requires `CLOUDFLARE_API_TOKEN` before first setup. The token is used to verify Cloudflare inventory and automate Email Routing checks, Email Sending checks, catch-all setup, and mailbox routing rule creation.\n\nRecommended permissions:\n\n- Account \u003e Account \u003e Read\n- Account \u003e Email Sending \u003e Read\n- Zone \u003e Zone \u003e Read\n- Zone \u003e Email Routing \u003e Read\n- Zone \u003e Email Routing \u003e Edit\n- Account \u003e Workers Scripts \u003e Read\n\nIf the token can access exactly one Cloudflare account, OmniDock detects that account automatically. If it can access multiple accounts, also add `CLOUDFLARE_ACCOUNT_ID`.\n\n## Cloudflare Build Variables\n\nAdd these under:\n\n`Worker \u003e Settings \u003e Build \u003e Build configuration \u003e Variables and secrets`\n\nThese values are build-time values. They are used to deploy the Worker with correct D1/R2 bindings. They are not app runtime secrets.\n\n| Name | Value to type | Required |\n| --- | --- | --- |\n| `OMNIDOCK_D1_DATABASE_ID` | Your D1 database id | Yes |\n| `OMNIDOCK_R2_BUCKET_NAME` | Your R2 bucket name, for example `omnidock-mail` | Yes |\n| `OMNIDOCK_D1_DATABASE_NAME` | D1 display name, for example `omnidock-db` | Optional |\n| `OMNIDOCK_EXTRA_R2_BUCKETS` | Extra R2 bucket names, for example `client-files,media-files` | Optional |\n| `WORKER_SCRIPT_NAME` | Deployed Worker script name, for example `omnidock` | Optional, only when your script name is different |\n| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account id | Only if the build token can access multiple accounts |\n\nIf these values are missing during a Git update, OmniDock may stop the deploy to protect existing `DB` and `MAIL_BUCKET` bindings from being removed. Extra R2 buckets are preserved when they already exist in your private `wrangler.jsonc`, are listed in `OMNIDOCK_EXTRA_R2_BUCKETS`, or can be read from Cloudflare by `node tools/deploy-preserving-bindings.mjs`.\n\n## Runtime Variables And Secrets\n\nAfter deploy, open:\n\n`Worker \u003e Settings \u003e Variables and Secrets \u003e Add`\n\nUse `Secret` only for sensitive values. Use plaintext variables for non-sensitive routing and display values.\n\nCloudflare does not create empty variable rows from the repository. Add one row for each value you need: choose `Type`, paste the exact `Name`, type your own `Value`, then save.\n\n| Type | Name | Value to type | When to add |\n| --- | --- | --- | --- |\n| Secret | `ADMIN_PASSWORD` | First admin password, at least 12 characters | Required before first setup |\n| Plaintext variable | `PRIMARY_DOMAIN` | First managed email domain, for example `example.com` | Required before first setup |\n| Secret | `CLOUDFLARE_API_TOKEN` | Cloudflare API token | Required before first setup |\n| Plaintext variable | `WORKER_SCRIPT_NAME` | Deployed Worker script name, for example `omnidock` | Add when OmniDock should create Email Routing rules |\n| Plaintext variable | `MANAGEMENT_HOST` | Custom dashboard hostname, for example `mail.example.com` | Optional |\n| Plaintext variable | `PASSWORD_RESET_FROM` | Verified reset sender, for example `no-reply@example.com` | Optional |\n| Plaintext variable | `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account id | Only if one token can access multiple accounts |\n\n`PRIMARY_DOMAIN`, `WORKER_SCRIPT_NAME`, `MANAGEMENT_HOST`, `PASSWORD_RESET_FROM`, and `CLOUDFLARE_ACCOUNT_ID` are not secrets.\n\nDo not add `ADMIN_PASSWORD` or `CLOUDFLARE_API_TOKEN` as plaintext variables.\n\n## Required Bindings\n\nD1, R2, Email Sending, and Workers AI are bindings, not secrets.\n\n| Binding name | Resource |\n| --- | --- |\n| `DB` | Cloudflare D1 database |\n| `MAIL_BUCKET` | Cloudflare R2 bucket |\n| `EMAIL` | Cloudflare Email Sending binding |\n| `AI` | Workers AI binding for Index Engine document-to-markdown and OCR-style extraction |\n\nThe running Worker must receive `DB` as a D1 binding and `MAIL_BUCKET` as an R2 binding. The `AI` binding is included in the public config so Index Engine can use Cloudflare Workers AI Markdown Conversion for supported PDFs, images, Office files, and spreadsheets. R2 bucket names are copied into runtime display variables during deploy so the Buckets UI can show real bucket names instead of binding names.\n\n### Extra R2 Buckets\n\n`MAIL_BUCKET` is the primary bucket used for raw email, attachments, and manual files. You can attach additional R2 buckets for file browsing and management.\n\nSimple path for extra buckets:\n\n1. Create or select the R2 buckets in Cloudflare.\n2. Add one build variable named `OMNIDOCK_EXTRA_R2_BUCKETS`.\n3. Put only the bucket names in the value, separated by commas.\n4. Deploy with `node tools/deploy-preserving-bindings.mjs` or `npm run deploy`.\n\n```dotenv\nOMNIDOCK_EXTRA_R2_BUCKETS=client-files,media-files\n```\n\nOmniDock automatically creates safe Worker binding names such as `R2_CLIENT_FILES` and `R2_MEDIA_FILES`, then shows the real bucket names in the Buckets dropdown.\n\nDo not open the Worker R2 binding form for this simple path. If Cloudflare asks you to choose an R2 bucket from a dropdown, you are in the manual Bindings screen. Cancel that form and add `OMNIDOCK_EXTRA_R2_BUCKETS` under Build configuration \u003e Variables and secrets instead.\n\nAdvanced custom binding names are still supported when you need them:\n\n```dotenv\nOMNIDOCK_EXTRA_R2_BUCKETS=FILES_BUCKET:client-files,MEDIA_BUCKET:media-files\n```\n\nKeep the Cloudflare deploy command as `node tools/deploy-preserving-bindings.mjs` or `npm run deploy`. A bare `npx wrangler deploy` can remove extra R2 bindings that only exist in the dashboard.\n\n## First Login\n\nAfter deploy:\n\n1. Open the Worker URL shown by Cloudflare.\n2. If OmniDock lists missing setup, add the listed bindings, variables, or secrets in Cloudflare Worker settings.\n3. Complete the setup screen with name, email, recovery email, primary domain, and admin password.\n4. The admin password must match the `ADMIN_PASSWORD` secret for first setup.\n5. OmniDock stores the password as a salted PBKDF2 hash in D1.\n6. Create mailbox addresses such as `support`, `info`, or `billing`.\n7. Use `Settings \u003e Rules` to route addresses or enable catch-all.\n8. Click `Sync` to refresh Cloudflare inventory, routing checks, and external inboxes.\n\nThe recovery email must be outside the primary domain. Use Gmail, iCloud, Outlook, a company mailbox, or another address that will still work if the managed domain has a routing issue.\n\n## Main App Areas\n\n### Mail\n\nThe Mail view supports inbox, sent, archive, search, rich compose, attachments, thread actions, and mailbox scoping. You can choose all mailboxes or a single mailbox from the top search area.\n\n### Rules\n\nRules manages Cloudflare zones, sending status, routing status, catch-all, mailbox routing rules, and the default domain. Domain creation is handled from Cloudflare sync; mailbox addresses are created for the selected domain.\n\n### Contacts\n\nContacts supports manual creation, one-by-one editing, deletion, phone numbers, company, tags, notes, and CSV/TXT/VCF imports with an import report.\n\n### Signatures\n\nSignatures are mailbox-based and support rich text, links, colors, and an HTML preview path. Enabled signatures are appended when composing from that mailbox.\n\n### External Accounts\n\nExternal account profiles let you document Gmail, Outlook, Yahoo, iCloud, or custom IMAP/SMTP settings. OmniDock stores the credential secret name and connection metadata in D1. Put real app passwords or OAuth secrets in Cloudflare Worker secrets, not in D1 and not in the repository.\n\nFor Gmail app passwords, create a Worker secret whose name is the Gmail address. In Cloudflare, set `Name` to `name@gmail.com` and `Value` to the Gmail app password. In OmniDock, add the same Gmail address as the external account. For multiple Gmail accounts, each account naturally gets its own secret name because each email address is unique.\n\nExternal inbox pulling is resumable. Pressing `Sync` queues D1-backed background jobs, starts a short immediate Worker run, and then the scheduled Worker continues jobs in small batches every 15 minutes. Scheduled maintenance intentionally runs one heavy task at a time: external mail pulls first, then R2 indexing only when no mail pull is active. If a mailbox is too large to finish in one run, OmniDock keeps the folder and IMAP UID cursor in D1; pressing `Sync` again continues from the saved cursor instead of starting over. Refreshing or closing the dashboard does not cancel the queued pull.\n\nExternal sending uses the configured SMTP profile for that account. Gmail, Outlook, Yahoo, iCloud, and custom providers can each have inbound sync, outbound send, or both enabled. Provider credentials stay in Worker secrets; the UI stores only the account metadata needed to connect.\n\n### Other Settings\n\nOther Settings controls automatic refresh. The default is 10 seconds and can be changed from the UI.\n\n### Buckets\n\nThe Buckets sidebar opens a dropdown of configured R2 buckets. `MAIL_BUCKET` is always the primary mail bucket. Extra buckets from `OMNIDOCK_EXTRA_R2_BUCKETS` appear in the same dropdown with their real bucket names. You can browse folder prefixes, create folders and nested folders, preview supported file types, upload files, download objects, and delete objects.\n\nBucket search reads filenames, paths, supported text files, extractable PDF text, and saved object text indexes. OCR and document extraction are handled by Settings \u003e Index Engine, not by the search button. Index Engine scans every configured R2 bucket, skips unchanged objects by ETag and size, extracts text into D1, and writes success or failure rows into Logs. When the Worker has the `AI` binding, supported scanned PDFs, images, Office documents, and spreadsheets are converted through Workers AI Markdown Conversion. Searches then use the saved D1 index instead of re-running OCR on every query.\n\nPreview support is built for common operator workflows:\n\n- Images open inline for quick visual checks.\n- PDFs open in an embedded preview frame.\n- Text files can be inspected without downloading.\n- Unsupported files remain downloadable.\n- R2 uploads show progress and per-file status so large batches are easier to monitor.\n- R2 deletes use an in-app confirmation dialog instead of browser-native alerts.\n\n## Custom Domain\n\nThe public template intentionally does not include a personal custom domain in `wrangler.jsonc`.\n\nTo use your own management host, add a custom domain in Cloudflare Workers, then set `MANAGEMENT_HOST` as a plaintext variable.\n\nYou can also leave `MANAGEMENT_HOST` blank and use the generated `workers.dev` URL.\n\n## Manual Install\n\nUse this path if you deploy from your own machine instead of Cloudflare Git deploy.\n\nInstall dependencies:\n\n```bash\nnpm install\n```\n\nCreate D1 and R2:\n\n```bash\nnpx wrangler d1 create omnidock-db\nnpx wrangler r2 bucket create omnidock-mail\n```\n\nFor a dashboard-managed install, add these resources in Cloudflare and keep the build variables set so updates do not remove them:\n\n- `DB` -\u003e the D1 database\n- `MAIL_BUCKET` -\u003e the R2 bucket\n- `EMAIL` -\u003e Cloudflare Email Sending\n\nFor a private Wrangler-managed install, add your own D1 `database_id` and R2 `bucket_name` to your private fork's `wrangler.jsonc`:\n\n```jsonc\n\"d1_databases\": [\n  {\n    \"binding\": \"DB\",\n    \"database_name\": \"omnidock-db\",\n    \"database_id\": \"your-d1-database-id\"\n  }\n],\n\"r2_buckets\": [\n  {\n    \"binding\": \"MAIL_BUCKET\",\n    \"bucket_name\": \"omnidock-mail\"\n  },\n  {\n    \"binding\": \"FILES_BUCKET\",\n    \"bucket_name\": \"client-files\"\n  }\n]\n```\n\nBuild and deploy:\n\n```bash\nnpm run deploy\n```\n\nIf your private `wrangler.jsonc` contains the `DB` binding and you want to run migrations explicitly before deploy, use:\n\n```bash\nnpm run deploy:with-migrations\n```\n\nWrangler secret equivalents:\n\n```bash\nnpx wrangler secret put ADMIN_PASSWORD\nnpx wrangler secret put CLOUDFLARE_API_TOKEN\n```\n\nSet plaintext variables such as `PRIMARY_DOMAIN`, `WORKER_SCRIPT_NAME`, `MANAGEMENT_HOST`, `PASSWORD_RESET_FROM`, and `CLOUDFLARE_ACCOUNT_ID` in the Cloudflare dashboard. `R2_BUCKET_NAME` and `EXTRA_R2_BUCKETS` are generated from the R2 deploy mapping unless you intentionally override the display names. For private installs only, you may keep plaintext values under `vars` in your private `wrangler.jsonc`; do not commit personal values to a public fork.\n\n## Local Development\n\nCreate `.dev.vars` only if you need local-only secret values:\n\n```bash\ntouch .dev.vars\n```\n\nDo not commit `.dev.vars`.\n\n```dotenv\n# optional local secrets\n# ADMIN_PASSWORD=\n# CLOUDFLARE_API_TOKEN=\n\n# optional local plaintext variables\n# PRIMARY_DOMAIN=\n# PASSWORD_RESET_FROM=no-reply@example.com\n```\n\nIf you want local sample data, add this only to your local `.dev.vars`:\n\n```dotenv\nENABLE_DEV_SEED=true\n```\n\nRun the Worker API:\n\n```bash\nnpm run dev:worker\n```\n\nRun the Vite UI:\n\n```bash\nnpm run dev\n```\n\nVite proxies `/api` to `http://127.0.0.1:8787`.\n\nFor local sample data after migrations:\n\n```bash\ncurl -X POST http://127.0.0.1:8787/api/dev/seed \\\n  -H \"Authorization: Bearer $ADMIN_PASSWORD\" \\\n  -H \"Content-Type: application/json\" \\\n  -d \"{}\"\n```\n\nThe seed endpoint is disabled unless `ENABLE_DEV_SEED=true`.\n\n## Architecture\n\n| Layer | Technology |\n| --- | --- |\n| UI | React + Vite |\n| Runtime | Cloudflare Workers |\n| Static assets | Workers assets binding |\n| Inbound email | Cloudflare Email Routing to Worker `email()` handler |\n| Outbound email | Cloudflare Email Sending binding |\n| Metadata | Cloudflare D1 |\n| Raw mail, attachments, manual files | Cloudflare R2 |\n| R2 text index | Cloudflare D1 plus optional Workers AI extraction |\n| Admin auth | D1-stored salted PBKDF2 password hash |\n| Cloudflare automation | Cloudflare API token stored as Worker secret |\n\n## Security Notes\n\n- Do not commit `.dev.vars`.\n- Do not commit API tokens, admin passwords, app passwords, OAuth secrets, D1 ids from private installs, or personal domains.\n- Use least-privilege Cloudflare API tokens.\n- Store external email credentials as Worker secrets; OmniDock stores only the secret name.\n- Password reset tokens are stored hashed in D1 and expire after 30 minutes.\n- OmniDock only sends from enabled D1 mailbox addresses on verified sending domains.\n- The browser does not store the admin password in web storage. Login creates a D1-backed admin session and returns an HttpOnly, SameSite cookie; only a hash of the session token is stored server-side.\n- The default public template has no custom domain, account id, D1 id, R2 bucket, token, password, or personal email baked into source control.\n\n## Useful Commands\n\n```bash\nnpm run types\nnpm run check\nnpm run build\nnpm run deploy\nnpm run db:migrate:local\nnpm run db:migrate:remote\n```\n\n## Public Repository Checklist\n\nBefore making your repository public:\n\n- Confirm `wrangler.jsonc` does not contain your personal account id, D1 id, R2 bucket name, custom domain, or personal email.\n- Confirm `.dev.vars` is not tracked.\n- Confirm docs/screenshots do not show private domains or real emails.\n- Confirm Cloudflare build variables use `OMNIDOCK_*` names.\n- Confirm the README uses the fork-first install flow and does not include a one-click deploy button.\n- Confirm `SECURITY.md`, `CONTRIBUTING.md`, `SUPPORT.md`, issue templates, PR template, and CI workflow are present.\n- Set GitHub repository description, website, topics, and social preview using [docs/GITHUB_SEO.md](docs/GITHUB_SEO.md).\n- Run `npm audit --audit-level=moderate`.\n- Run `npm run build`.\n\n## GitHub Repository SEO\n\nGitHub does not read a special SEO file for repository topics. Set these fields manually in the GitHub repository sidebar after publishing.\n\nRecommended repository description:\n\n```text\nOpen-source Cloudflare email dashboard for Workers, Email Routing, Email Sending, D1, R2, support inboxes, contacts, signatures, Gmail sync, and R2 file management.\n```\n\nRecommended topics:\n\n```text\ncloudflare cloudflare-workers cloudflare-email-routing cloudflare-email-sending cloudflare-d1 cloudflare-r2 workers-ai email-dashboard support-inbox self-hosted-email email-routing email-sending r2-storage r2-bucket-manager d1-database gmail-sync imap smtp pdf-preview ocr-indexing document-search react typescript serverless open-source\n```\n\nRecommended website URL:\n\n```text\nhttps://omnidock.org\n```\n\nRecommended social preview:\n\n```text\nUse docs/brand/omnidock-social-preview.svg as the source artwork. Export it to PNG before uploading it to GitHub Settings \u003e Social preview.\n```\n\nSee [docs/GITHUB_SEO.md](docs/GITHUB_SEO.md) for a complete GitHub launch checklist, About text, pinned issue ideas, release title examples, and search-friendly copy.\n\n## License\n\nOmniDock is released under the MIT License. See [LICENSE](LICENSE).\n\n## Product SEO Copy\n\nMeta title:\n\n```text\nOmniDock - Open-source Cloudflare email dashboard\n```\n\nMeta description:\n\n```text\nOmniDock is a self-hosted Cloudflare Workers email dashboard for Email Routing, Email Sending, D1, R2, support inboxes, contacts, signatures, Gmail sync, and R2 file management.\n```\n\nShort product pitch:\n\n```text\nOmniDock turns Cloudflare Workers, Email Routing, Email Sending, D1, R2, and Workers AI into a private email operations dashboard for support inboxes, multi-domain routing, Gmail and external IMAP/SMTP sync, contacts, signatures, attachments, logs, R2 bucket management, file preview, upload workflows, and indexed OCR/document search.\n```\n\nSearch phrases this README intentionally covers:\n\n```text\nCloudflare email dashboard, Cloudflare Workers email app, Cloudflare Email Routing UI, Cloudflare Email Sending dashboard, open-source support inbox, self-hosted email management, D1 email database, R2 attachment storage, R2 file manager, R2 bucket manager, Workers AI document extraction, Gmail IMAP sync, external SMTP sending, PDF preview, attachment preview, OCR text indexing, serverless email dashboard, multi-domain email inbox.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fufukayyildiz%2Fomnidock","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fufukayyildiz%2Fomnidock","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fufukayyildiz%2Fomnidock/lists"}