{"id":51457015,"url":"https://github.com/hyprlab/tspro-relay","last_synced_at":"2026-07-06T01:02:54.181Z","repository":{"id":361586769,"uuid":"1254469906","full_name":"hyprlab/tspro-relay","owner":"hyprlab","description":"A small companion app that lets the main TS Pro app send mail on hosts that block outbound SMTP ports. Built with Claude Code.","archived":false,"fork":false,"pushed_at":"2026-07-04T13:47:01.000Z","size":85,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-04T15:27:50.150Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://gettspro.com","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hyprlab.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","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":"2026-05-30T15:53:45.000Z","updated_at":"2026-07-04T13:47:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hyprlab/tspro-relay","commit_stats":null,"previous_names":["viibeware/tspro-relay","hyprlab/tspro-relay"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/hyprlab/tspro-relay","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyprlab%2Ftspro-relay","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyprlab%2Ftspro-relay/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyprlab%2Ftspro-relay/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyprlab%2Ftspro-relay/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hyprlab","download_url":"https://codeload.github.com/hyprlab/tspro-relay/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hyprlab%2Ftspro-relay/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35174071,"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-07-05T02:00:06.290Z","response_time":100,"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":[],"created_at":"2026-07-06T01:02:53.866Z","updated_at":"2026-07-06T01:02:54.137Z","avatar_url":"https://github.com/hyprlab.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TS Pro Relay\n\n[![Docker Hub](https://img.shields.io/badge/docker-hyprlab%2Ftspro--relay-2496ED?logo=docker\u0026logoColor=white)](https://hub.docker.com/r/hyprlab/tspro-relay)\n[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)\n\nA small self-hosted **outbound email relay** for\n[Trusted Servants Pro](https://hub.docker.com/r/hyprlab/trusted-servants-pro),\nfor running the portal on hosts that **block outbound SMTP ports**\n(25/465/587) — most notably DigitalOcean droplets, but also many other\ncloud providers.\n\nInstead of the app connecting to an SMTP server directly, it POSTs each\nmessage as JSON to this relay over **HTTPS** (behind a reverse proxy).\nThe relay runs somewhere with SMTP egress and performs the actual\ndelivery. SMTP credentials live only on the relay, never in the app's\ndatabase.\n\n```\nTSP app  ──HTTPS──▶  TS Pro Relay  ──SMTP:587/465──▶  mail server\n(no SMTP egress)     (this repo)                      (Gmail, SES, …)\n```\n\n## Admin interface\n\nThe relay ships a **web interface with a login** so an operator can set\neverything up without editing JSON or env files:\n\n- **Transaction Log** — every send (and unauthorized attempt) with\n  status, sender, recipients, subject, and any error. Counters for\n  total / sent / failed / unauthorized.\n- **Settings** — the upstream SMTP server, a one-click **API key**\n  (reveal / copy / regenerate), an allowed-sender allowlist, attachment\n  size limit, a **Send test email** button, optional **Cloudflare\n  Turnstile** bot protection on the login page, and the **admin\n  password**.\n\nConfiguration and the log are stored in a SQLite DB on the `./data`\nvolume. The SMTP password and API key are encrypted at rest with a key\nderived from `RELAY_SECRET_KEY`.\n\n## Install\n\nThe published image is on Docker Hub as\n[`hyprlab/tspro-relay`](https://hub.docker.com/r/hyprlab/tspro-relay).\nYou don't need to clone this repo to run it — just a `docker-compose.yml`\nand a `.env`.\n\n### 1. Create a working directory\n\n```bash\nmkdir tspro-relay \u0026\u0026 cd tspro-relay\n```\n\n### 2. Create `docker-compose.yml`\n\n```yaml\nservices:\n  relay:\n    image: hyprlab/tspro-relay:latest\n    # The relay serves BOTH the admin UI and the JSON send API on one port.\n    # In production put a TLS-terminating reverse proxy in front (see below)\n    # and have the TSP app POST to the https:// URL.\n    ports:\n      - \"0.0.0.0:8026:8000\"\n    environment:\n      # Signs sessions AND derives the at-rest encryption key for the\n      # stored SMTP password + API key. REQUIRED — set a long random value.\n      #   python -c \"import secrets; print(secrets.token_urlsafe(48))\"\n      - RELAY_SECRET_KEY=${RELAY_SECRET_KEY:?set RELAY_SECRET_KEY in .env}\n      # First-boot admin login (ignored once the admin row exists).\n      - RELAY_ADMIN_USER=${RELAY_ADMIN_USER:-admin}\n      - RELAY_ADMIN_PASSWORD=${RELAY_ADMIN_PASSWORD:-admin}\n      - RELAY_LOG_LEVEL=${RELAY_LOG_LEVEL:-INFO}\n      # Set to 1 ONLY for local HTTP testing without TLS.\n      - RELAY_INSECURE_COOKIES=${RELAY_INSECURE_COOKIES:-}\n    volumes:\n      - ./data:/data        # relay.db (settings, admin, transaction log)\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD\", \"python\", \"-c\", \"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz',timeout=5).status==200 else 1)\"]\n      interval: 30s\n      timeout: 5s\n      retries: 3\n      start_period: 10s\n```\n\n### 3. Create `.env`\n\n```bash\n# Signs login sessions AND encrypts the stored SMTP password + API key.\n# REQUIRED. Generate a strong value:\n#   python -c \"import secrets; print(secrets.token_urlsafe(48))\"\nRELAY_SECRET_KEY=replace-with-a-long-random-value\n\n# First-boot admin login (change the password from the UI afterwards).\nRELAY_ADMIN_USER=admin\nRELAY_ADMIN_PASSWORD=change-me-on-first-login\n```\n\n### 4. Start it\n\n```bash\ndocker compose up -d\n```\n\nThe relay (UI + API) is now on **port 8026**. Open `http://\u003chost\u003e:8026`,\nsign in, and on **Settings** fill in your SMTP server and copy the API\nkey.\n\n\u003e **Building from source instead?** Clone this repo and use\n\u003e `image:` → `build: .` in the compose file, then\n\u003e `docker compose up -d --build`.\n\n## TLS in production\n\nThe login cookie and Bearer token must never cross plaintext. Put a\nreverse proxy in front that terminates HTTPS and proxies to\n`127.0.0.1:8026`.\n\n**Caddy**\n```\nrelay.example.com {\n    reverse_proxy 127.0.0.1:8026\n}\n```\n\n**nginx**\n```\nlocation / {\n    proxy_pass http://127.0.0.1:8026;\n    proxy_set_header Host $host;\n    proxy_set_header X-Forwarded-For $remote_addr;\n}\n```\n\nThen point the TSP app at `https://relay.example.com`.\n\n## Configure the TSP app\n\nIn the portal: **Settings → Domain / Email**\n\n1. **Sending method** → *API relay (HTTPS)*\n2. **Relay URL** → `https://relay.example.com`\n3. **Relay API key** → the key from the relay's Settings page\n4. **From email / From name** → your sender identity\n5. **Save Email Settings**, then **Send Test**. The result also lands in\n   the relay's Transaction Log.\n\n## API (consumed by the TSP app)\n\n### `POST /api/send`\nHeader: `Authorization: Bearer \u003capi-key\u003e` · Body: JSON\n\n```json\n{\n  \"from_email\": \"noreply@example.com\",\n  \"from_name\": \"Trusted Servants Pro\",\n  \"to\": [\"someone@example.org\"],\n  \"subject\": \"Hello\",\n  \"text\": \"Plain-text body\",\n  \"html\": \"\u003cp\u003eOptional HTML body\u003c/p\u003e\",\n  \"reply_to\": \"replies@example.org\",\n  \"reply_to_name\": \"Replies\",\n  \"attachments\": [\n    {\"filename\": \"doc.pdf\", \"mime_type\": \"application/pdf\", \"content_b64\": \"...\"}\n  ]\n}\n```\n\n`200 {\"ok\": true}` on success; otherwise `{\"ok\": false, \"error\": \"...\"}`\nwith `401` (bad key), `403` (From not allowed), `413` (attachments too\nbig), or `502` (SMTP failed).\n\n### `GET /healthz`\nUnauthenticated liveness probe; reports whether SMTP + an API key are\nconfigured, without leaking secrets.\n\n## Environment variables\n\n| Var | Required | Default | Notes |\n|-----|----------|---------|-------|\n| `RELAY_SECRET_KEY` | ✅ | — | Signs sessions + encrypts stored secrets. Keep it stable — rotating it invalidates the stored SMTP password + API key. |\n| `RELAY_ADMIN_USER` | | `admin` | First-boot admin username. |\n| `RELAY_ADMIN_PASSWORD` | | `admin` | First-boot password — change it in the UI. |\n| `RELAY_LOG_LEVEL` | | `INFO` | `DEBUG` \\| `INFO` \\| `WARNING` \\| `ERROR`. |\n| `RELAY_INSECURE_COOKIES` | | — | Set `1` only for local HTTP testing (no TLS). |\n| `RELAY_DATA_DIR` | | `/data` | Where `relay.db` lives. |\n\nEverything else (SMTP host/port/security/credentials, API key, allowed\nsenders, attachment limit, Turnstile keys) is managed from the\n**Settings** page.\n\n## Local end-to-end test\n\n`docker-compose.test.yml` (in this repo) brings up the relay built from\nsource plus a **Mailpit** SMTP sink to verify delivery. Mailpit's inbox\nis bound to localhost only; the relay UI is on the LAN. See the comments\nat the top of that file.\n\n## Security notes\n\n- Always run the UI + API behind TLS in production.\n- Change the seeded admin password immediately (Settings → Admin account).\n- Keep `RELAY_SECRET_KEY` long, random, and stable.\n- Use the **Allowed From** list so a leaked key can't be used to spoof\n  arbitrary senders.\n- Optionally enable **Cloudflare Turnstile** (Settings → Login bot\n  protection) to challenge the sign-in page. The relay needs outbound\n  HTTPS to `challenges.cloudflare.com` for verification.\n\n## License\n\nReleased under the **GNU Affero General Public License v3.0** — see\n[LICENSE](LICENSE). If you run a modified version as a network service,\nthe AGPL requires you to offer your users the corresponding source.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhyprlab%2Ftspro-relay","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhyprlab%2Ftspro-relay","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhyprlab%2Ftspro-relay/lists"}