{"id":31240340,"url":"https://github.com/reeveskeefe/openinboundemail","last_synced_at":"2026-05-09T05:33:41.994Z","repository":{"id":313846064,"uuid":"1053125203","full_name":"reeveskeefe/OpenInboundEmail","owner":"reeveskeefe","description":"openInboundmail: self‑hosted inbound SMTP server with a web panel. Generates DNS artifacts (MX, MTA‑STS, TLS‑RPT), optional Cloudflare apply, and stores mail in Maildir. Dev on 2525; Prod on 25. NEEDS SECURITY HARDENING, AND PRODUCTION LEVEL TESTS TO ASSURE IT WORKS, STILL UNDER DEVELOPMENT. ","archived":false,"fork":false,"pushed_at":"2025-09-09T03:10:14.000Z","size":649,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-09T06:19:04.399Z","etag":null,"topics":["email","email-client-server","email-receiver","from","inbound","inbound-only","js","node","reciever","scratch","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/reeveskeefe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"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":"2025-09-09T03:01:38.000Z","updated_at":"2025-09-09T03:16:01.000Z","dependencies_parsed_at":"2025-09-09T06:19:45.790Z","dependency_job_id":"83dcd595-8986-4599-a6bd-71c20ed5f4f1","html_url":"https://github.com/reeveskeefe/OpenInboundEmail","commit_stats":null,"previous_names":["reeveskeefe/openinboundemail"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/reeveskeefe/OpenInboundEmail","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reeveskeefe%2FOpenInboundEmail","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reeveskeefe%2FOpenInboundEmail/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reeveskeefe%2FOpenInboundEmail/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reeveskeefe%2FOpenInboundEmail/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/reeveskeefe","download_url":"https://codeload.github.com/reeveskeefe/OpenInboundEmail/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reeveskeefe%2FOpenInboundEmail/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":276487458,"owners_count":25651133,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-22T02:00:08.972Z","response_time":79,"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":["email","email-client-server","email-receiver","from","inbound","inbound-only","js","node","reciever","scratch","typescript"],"created_at":"2025-09-22T22:30:40.699Z","updated_at":"2026-05-09T05:33:41.976Z","avatar_url":"https://github.com/reeveskeefe.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# openInboundmail — Receive‑only SMTP + DNS Artifact Pack + React Settings Panel\n\n\u003e Status and security note\n\u003e\n\u003e - This repository is provided as-is. Localhost/dev mode is tested; production mode is not yet field-tested, but should work in theory. \n\u003e - Before exposing publicly, review the Security hardening section, set an ADMIN_TOKEN, set FRONTEND_ORIGIN, and enable STARTTLS for SMTP.\n\u003e - Never commit secrets (API tokens, .env files, data/state.json, or mail spools). A .gitignore is included to help prevent this.\n\nopenInboundmail is a minimal, production‑ready inbound email stack:\n- SMTP server (receive‑only) with greylisting and optional DMARC enforcement\n- Fastify API with DNS preview, live status, and a downloadable DNS Artifact Pack (.zip)\n- React settings panel (Vite + Tailwind) to configure domain, hostnames, IPs, and policies\n- Optional Cloudflare apply; Manual mode works with any DNS provider\n\n## Dashboard Preview\n\n![Dashboard Overview](apps/web/dashboard1.png)\n*Main configuration panel with domain settings, DNS provider options, and policy controls*\n\n![Dashboard Status View](apps/web/dashboard2.png)\n*DNS status monitoring, record preview, and development testing tools*\n\n## Monorepo layout\n\n```\ninboundemail/\n├─ package.json           # workspaces orchestrator\n├─ apps/\n│  ├─ server/            # Fastify API + SMTP server + DNS maintainer\n│  └─ web/               # Vite + React settings panel\n└─ README.md\n```\n\n## Quick start (Dev)\n\n```bash\n# install deps at the monorepo root\nnpm i\nnpm --workspace apps/server install\nnpm --workspace apps/server run build\nnpm run dev\n\n```\n\nOpen http://localhost:5174 — the panel talks to http://localhost:4000.\n- Dev mode uses SMTP port 2525 and never writes DNS. Use swaks to test locally.\n- Switch to Production in the panel when you're ready to bind SMTP on port 25.\n\nSingle master inbox account\n---------------------------\n\nThis project exposes a small master inbox account used to view received messages. Behavior in dev:\n---------------------------\n\nThis project exposes a small master inbox account used to view received messages. Behavior in dev:\n\n- On server start the code will check for an existing account file and, if none is present, the UI will surface a \"Create master account\" form.\n- The create flow stores a single master account on disk (file: `apps/server/data/admin.json`) and returns a long-lived token to the client. The client stores that token in `localStorage.INBOX_TOKEN` and sends it as `Authorization: Bearer \u003ctoken\u003e` when calling `/api/inbox`.\n- If an account already exists the UI will show a Login flow which exchanges username+password for a regenerated token.\n- Alternatively you can still set `INBOX_PASSWORD` in the server environment to gate the inbox endpoints; the server will also accept that password as Bearer token.\n\nImportant: only one master account is supported by design. To rotate the token or reset the account, remove the `apps/server/data/admin.json` file and recreate the account via the UI or API.\n\nNote: the server may prompt for an inbox password at startup if `INBOX_PASSWORD` is not set in your environment; just press Enter to skip and use the account flow instead.\n\n## Publish to GitHub (safely)\n\n1) Create a new empty repo on GitHub (do not add a README or .gitignore there).\n2) Ensure you do NOT have secrets tracked locally:\n\t- .env files, `data/state.json`, and `apps/server/spool/` should be ignored (see .gitignore). If they show up in `git status`, move or delete them before committing.\n3) Initialize and push:\n\n```bash\ngit init\ngit add .\ngit commit -m \"feat: initial openInboundmail monorepo\"\ngit branch -M main\ngit remote add origin git@github.com:\u003cyou\u003e/\u003crepo\u003e.git\ngit push -u origin main\n```\n\nAfter pushing, go to the repo settings and set required branch protection as you prefer. If you later add secrets in CI, store them only in GitHub Actions secrets, not in the repo.\n\nReady-to-push checklist\n-----------------------\n\nBefore you run the `git` commands above, confirm these locally to avoid pushing secrets or runtime state:\n\n- Ensure `.env` files are not committed. If you have a local `.env`, add it to `.gitignore` and remove from the index: `git rm --cached .env`.\n- Ensure any runtime `apps/server/data/` files (including `admin.json`) are not tracked. You can add `apps/server/data/` to `.gitignore` if it isn't already.\n- Ensure `apps/server/spool/` (mail spools) is not tracked. If needed: `git rm -r --cached apps/server/spool`.\n- Run `git status` and confirm only code/config files are staged.\n\nWhen those checks are clean, commit and push as described above.\n\n## Configure your domain\n\nIn the panel (http://localhost:5174):\n- Domain: your apex domain, e.g. `example.com`\n- SMTP Hostname (MX target): a host inside your domain, e.g. `mx1.example.com`\n- Public IPv4/IPv6: public IPs of your SMTP host (used for A/AAAA)\n- Recipients: leave empty to accept any user @ domain, or list full addresses (e.g. `info@example.com`)\n- TLS‑RPT aggregate email: where TLS reports should go (e.g. `tlsrpt@example.com`)\n- MTA‑STS mode: `enforce` (recommended), `testing`, or `none`\n- Policies: toggle `Greylisting` and `Require DMARC=pass` as desired\n\nSave \u0026 Restart SMTP to apply changes. In Dev it restarts on port 2525.\n\n### Why you might still see example.com in DNS Preview/Status\n\nThe app starts with placeholder defaults (`example.com`, `mx1.example.com`, `tlsrpt@example.com`). If DNS Preview/Status shows those values:\n\n- Update all three fields in Settings:\n\t- Domain → your real domain (e.g., `yourdomain.com`).\n\t- SMTP Hostname → the actual MX target you want (e.g., `mx1.yourdomain.com`). If this hostname is inside your domain, A/AAAA records for it will also be generated. If it's outside (e.g., `mail.example.net`), A/AAAA won't be generated here and should be managed where that host lives.\n\t- TLS‑RPT aggregate email → a real mailbox you monitor (e.g., `tlsrpt@yourdomain.com`).\n- Click Save \u0026 Restart SMTP.\n- Refresh the DNS Preview/Status cards; the example.com placeholders should disappear.\n\nTip: DNS Artifacts and any Cloudflare apply use the values currently saved in Settings—double‑check them before applying.\n\n### Recipients field: catch‑all vs allow‑list\n\nThis server is receive‑only and can act as either:\n\n- Catch‑all (accept any user at your domain): Leave the Recipients field completely empty. The server will accept any address that ends with `@\u003cyour-domain\u003e`. Example: `anylocalpart@yourdomain.com` will be accepted and stored under `apps/server/spool/yourdomain.com/\u003clocalpart\u003e/Maildir/new/`.\n- Allow‑list (only specific mailboxes): Enter a comma‑separated list of full email addresses. Only those exact addresses will be accepted; all others get `550 5.1.1 mailbox unavailable`.\n\nExamples (what to type):\n- Catch‑all: leave the field blank.\n- Allow‑list: `info@yourdomain.com, support@yourdomain.com, billing@yourdomain.com`\n\nNotes:\n- Use full addresses, not just local parts. Correct: `info@yourdomain.com`. Incorrect: `info`.\n- Matching is case‑insensitive and exact for the full address when allow‑listing.\n- You can change this any time; click Save \u0026 Restart SMTP to apply.\n\n## DNS: Artifact Pack (.zip)\n\nClick \"Download Artifact Pack (.zip)\" to get ready‑to‑import files:\n- BIND zone file snippet: `dns/bind/\u003cdomain\u003e.zone`\n- Route53 change batch: `dns/route53-change-batch.json`\n- Generic CSV (name,type,ttl,priority,content): `dns/records.csv`\n- PowerDNS SQL (annotated): `dns/powerdns.sql`\n- MTA‑STS policy file: `mta-sts/.well-known/mta-sts.txt`\n- README‑DNS with copy/paste commands and dig checks: `dns/README-DNS.md`\n\nRecords generated:\n- MX at `@` → your SMTP hostname (e.g. `mx1.example.com.`)\n- A/AAAA for the MX hostname (only if it's inside your domain)\n- TXT at `_mta-sts.\u003cdomain\u003e` with `v=STSv1; id=...`\n- TXT at `_smtp._tls.\u003cdomain\u003e` with `v=TLSRPTv1; rua=mailto:...`\n- A/AAAA for `mta-sts.\u003cdomain\u003e` to serve the policy endpoint from this API\n\nApply them with your DNS provider and wait for propagation.\n\n## Optional: Cloudflare apply\n\nPrefer Manual. If you use Cloudflare:\n1) In the panel → DNS Provider → Cloudflare\n2) Paste an API token with DNS:Edit on your zone\n3) Click \"Apply via Cloudflare\"\n\nNotes:\n- The app normalizes trailing dots and compares content for idempotent updates.\n- Ensure A/AAAA records for your MX host are set to DNS‑only (no orange cloud). SMTP cannot be proxied.\n- Auto‑maintain can re‑upsert your records every 10 minutes (optional).\n\n## Verifying DNS\n\nUse dig to check propagation:\n\n```bash\ndig +short MX example.com\ndig +short A mx1.example.com\ndig +short AAAA mx1.example.com\ndig +short TXT _mta-sts.example.com\ndig +short TXT _smtp._tls.example.com\ncurl -s https://mta-sts.example.com/.well-known/mta-sts.txt\n```\n\nThen click \"Refresh Status\" in the panel.\n\n## Testing incoming email\n\nDev (localhost, SMTP on 2525):\n\n```bash\nswaks --server 127.0.0.1:2525 \\\n\t--from a@test.net \\\n\t--to info@example.com \\\n\t--data \"Subject: hi\\n\\nhello\"\n```\n\nCheck messages under:\n```\napps/server/spool/\u003cdomain\u003e/\u003cuser\u003e/Maildir/new/\n```\n\nProduction (public, SMTP on 25):\n1) Switch panel to Production.\n2) Ensure inbound TCP/25 is open and reachable on your public IP.\n3) From an external network, send to your domain or use swaks pointing to the MX host on port 25.\n\nGreylisting tip: disable it for first tests to avoid initial 450 tempfail, or resend after the min delay.\n\n## Running in Production\n\n- Open inbound TCP 25 on your firewall / cloud SGs.\n- Run the server as a systemd service or a container.\n- If binding privileged port 25 as non‑root, grant Node the capability:\n\t```bash\n\tsudo setcap 'cap_net_bind_service=+ep' $(which node)\n\t```\n- Set reverse DNS (PTR) for your public IP to your MX hostname if your provider allows it.\n- Keep time in sync (NTP) and consider TLS certs for the API host if serving MTA‑STS over HTTPS yourself.\n\n## Security hardening\n\n- Admin Token (API writes): Set ADMIN_TOKEN in `apps/server/.env` to require `Authorization: Bearer \u003ctoken\u003e` for settings updates and Cloudflare apply. The UI reads a token from `localStorage.ADMIN_TOKEN` for these calls.\n- Security headers: Helmet is enabled; CSP is disabled by default to avoid blocking the panel. You can harden CSP if self‑hosting static panel files.\n- Rate limiting: Basic per‑IP limit on API routes (200/min). Adjust via FASTIFY rate‑limit settings.\n- CORS: By default, allows only `FRONTEND_ORIGIN` (or localhost:5174 in dev). Set `FRONTEND_ORIGIN` explicitly in production.\n- STARTTLS: Provide `SMTP_TLS_KEY` and `SMTP_TLS_CERT` to enable STARTTLS for inbound SMTP.\n- DNSBL (RBL): When `ENABLE_RBL=true` and zones are configured, connections listed in any zone get `554 5.7.1 access denied` at connect.\n- DMARC enforcement: Enable \"Require DMARC=pass\" to bounce messages failing DMARC (when authentication results are available).\n- Token redaction: The API redacts stored Cloudflare tokens on GET /api/settings and preserves the existing token if the UI sends `__REDACTED__` on POST.\n\nEncrypted admin token storage\n-----------------------------\n\nYou can encrypt the master admin store by setting `ADMIN_STORE_KEY` in the server environment (keep this secret). When set, the server will store an encrypted `apps/server/data/admin.enc` file instead of plaintext `admin.json`. Use a long, random key (e.g. a 32+ char passphrase). Example for a systemd environment file:\n\n```ini\nADMIN_STORE_KEY=your-very-long-secret-key\n```\n\nIf `ADMIN_STORE_KEY` is not set, the server will fall back to the plaintext `apps/server/data/admin.json` (still file-permission protected). To rotate or remove the encrypted store, delete the `admin.enc`/`admin.json` file and recreate the account via the UI.\n\nSystemd service example\n-----------------------\n\nAn example systemd unit is included at `deploy/openinbound.service`. It demonstrates a minimal production deployment that runs the monorepo `start:prod` helper and reads environment variables from `/etc/openinbound.env`:\n\n```\n[Unit]\nDescription=OpenInboundEmail API and SMTP\nAfter=network.target\n\n[Service]\nType=simple\nUser=inbound\nGroup=inbound\nWorkingDirectory=/opt/openinbound\nEnvironmentFile=/etc/openinbound.env\nExecStart=/usr/bin/npm run start:prod\nRestart=on-failure\nRestartSec=5s\n\n[Install]\nWantedBy=multi-user.target\n```\n\nAdjust `WorkingDirectory`, `User`, and path to `npm` as appropriate for your host. Put secrets (for example `ADMIN_STORE_KEY`, `ADMIN_TOKEN`, `CF_API_TOKEN`) in `/etc/openinbound.env` and protect that file with strict permissions.\n\nRotate admin token safely\n------------------------\n\nA helper script is provided to regenerate the admin token and print it to stdout. It uses the same server store and writes the new token into the encrypted/plain store. Example:\n\n```bash\n# run from the repo or copy tools/rotate-admin-token.ts to the host\ntsx tools/rotate-admin-token.ts\n```\n\nThe script prints the new token; copy it into your secure vault immediately (or email to yourself temporarily). The server will start accepting the new token immediately.\n\nAutomated encrypted backups\n---------------------------\n\nWe provide a backup script and systemd timer that creates encrypted tarballs of `apps/server/data`. Files:\n- `tools/backup-data.sh` — creates a timestamped tar.gz and encrypts with `ADMIN_STORE_KEY` when present.\n- `deploy/openinbound-backup.service` — systemd oneshot to run the backup.\n- `deploy/openinbound-backup.timer` — systemd timer to run the backup daily.\n\nInstall and enable the timer on your host:\n\n```bash\nsudo cp deploy/openinbound-backup.* /etc/systemd/system/\nsudo systemctl daemon-reload\nsudo systemctl enable --now openinbound-backup.timer\n```\n\nHardened systemd unit notes\n---------------------------\n\nThe provided `deploy/openinbound.service` contains additional hardening options:\n- `PrivateTmp=true` — private /tmp for the service\n- `NoNewPrivileges=true` — disallow privilege escalation\n- `ProtectSystem=strict` and `ProtectHome=yes` — read-only /usr and protected home\n- `CapabilityBoundingSet=CAP_NET_BIND_SERVICE` — only allow binding privileged ports\n- `MemoryHigh`/`CPUAccounting` — soft resource limits\n\nAdjust these as needed for your environment. Keep secrets in `/etc/openinbound.env` and protect them with `chmod 600`.\n\n## Troubleshooting\n\n- \"Could not find declaration for 'mailauth'\" — already handled via a local d.ts in `apps/server/src/types-ext/mailauth.d.ts`.\n- \"Port 5173 in use\" — the panel runs on 5174. Use `npm run dev` again after freeing ports.\n- Mail not arriving? Check: MX points to your MX host, host A/AAAA is DNS‑only (Cloudflare), port 25 reachable, Greylisting disabled for first tests, and recipients configured correctly.\n\n## License\n\nMIT — openInboundmail is provided as‑is; contributions welcome.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freeveskeefe%2Fopeninboundemail","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freeveskeefe%2Fopeninboundemail","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freeveskeefe%2Fopeninboundemail/lists"}