From 057dbe9e360c014b30a76e83fd4be02f7ff4714a Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:58:47 -0600 Subject: [PATCH 1/2] Refresh runtime and consolidate CI workflows --- .github/workflows/build.yml | 360 ++++++++++++++++-- .github/workflows/check-upstream.yml | 120 ++++++ .github/workflows/sync-template.yml | 36 -- CHANGELOG.md | 11 + Dockerfile | 58 ++- README.md | 94 ++--- RELEASE-NOTES.md | 28 +- SECURITY.md | 20 + build-status.md | 3 + docs/simplelogin-setup.md | 91 +++-- renovate.json | 22 +- .../etc/cont-init.d/01-bootstrap-databases.sh | 61 +-- rootfs/etc/cont-init.d/01-validate-env.sh | 89 ++--- rootfs/etc/cont-init.d/03-write-env.sh | 188 +++------ .../etc/cont-init.d/04-configure-postfix.sh | 123 +++--- rootfs/etc/cont-init.d/04-db-migrate.sh | 96 ----- rootfs/etc/cont-init.d/05-generate-jwt.sh | 17 +- rootfs/etc/services.d/postfix/run | 9 + rootfs/etc/services.d/postgres/run | 13 +- rootfs/etc/services.d/redis/run | 12 +- rootfs/etc/services.d/simplelogin-email/run | 9 +- rootfs/etc/services.d/simplelogin-job/run | 9 +- rootfs/etc/services.d/simplelogin-web/run | 50 ++- scripts/check-upstream.py | 201 ++++++++++ scripts/smoke-test.sh | 94 +++++ upstream.toml | 11 + 26 files changed, 1264 insertions(+), 561 deletions(-) create mode 100644 .github/workflows/check-upstream.yml delete mode 100644 .github/workflows/sync-template.yml create mode 100644 CHANGELOG.md create mode 100644 SECURITY.md create mode 100644 build-status.md mode change 100755 => 100644 rootfs/etc/cont-init.d/01-bootstrap-databases.sh mode change 100755 => 100644 rootfs/etc/cont-init.d/04-configure-postfix.sh delete mode 100644 rootfs/etc/cont-init.d/04-db-migrate.sh mode change 100755 => 100644 rootfs/etc/cont-init.d/05-generate-jwt.sh create mode 100644 rootfs/etc/services.d/postfix/run mode change 100755 => 100644 rootfs/etc/services.d/postgres/run mode change 100755 => 100644 rootfs/etc/services.d/redis/run create mode 100755 scripts/check-upstream.py create mode 100755 scripts/smoke-test.sh create mode 100644 upstream.toml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6a9fa2d..73ac8ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,56 +1,364 @@ -name: Build SimpleLogin AIO +name: CI / SimpleLogin-AIO + on: push: branches: [ main ] - schedule: - - cron: '0 3 * * 0' + paths: + - 'Dockerfile' + - 'rootfs/**' + - 'scripts/**' + - 'upstream.toml' + - 'simplelogin-aio.xml' + - 'renovate.json' + - '.github/workflows/**' + pull_request: + branches: [ main ] + paths: + - 'Dockerfile' + - 'rootfs/**' + - 'scripts/**' + - 'upstream.toml' + - 'simplelogin-aio.xml' + - 'renovate.json' + - '.github/workflows/**' workflow_dispatch: inputs: - sl_version: - description: 'Optional: Specific SimpleLogin version to build (e.g., v3.4.0)' + run_smoke_test: + description: "Run the smoke-test job" + required: false + default: true + type: boolean + publish_image: + description: "Publish image tags from the current ref" required: false + default: false + type: boolean env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + REGISTRY: ghcr.io + IMAGE_NAME: jsonbored/simplelogin-aio + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - build: + detect-changes: runs-on: ubuntu-latest permissions: contents: read - packages: write - attestations: write - id-token: write + outputs: + build_related: ${{ steps.filter.outputs.build_related }} + xml_related: ${{ steps.filter.outputs.xml_related }} + renovate_related: ${{ steps.filter.outputs.renovate_related }} steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 - - name: Fetch Latest Upstream Version - id: get_version + - name: Classify changed files + id: filter + env: + EVENT_NAME: ${{ github.event_name }} + BEFORE_SHA: ${{ github.event.before || '' }} + BASE_SHA: ${{ github.event.pull_request.base.sha || '' }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + GITHUB_SHA_VALUE: ${{ github.sha }} run: | - if [ -n "${{ github.event.inputs.sl_version }}" ]; then - echo "VERSION=${{ github.event.inputs.sl_version }}" >> $GITHUB_ENV + if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + echo "build_related=true" >> "${GITHUB_OUTPUT}" + echo "xml_related=true" >> "${GITHUB_OUTPUT}" + echo "renovate_related=true" >> "${GITHUB_OUTPUT}" + exit 0 + fi + + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + base="${BASE_SHA}" + head="${HEAD_SHA}" else - LATEST=$(curl -s https://api.github.com/repos/simple-login/app/releases/latest | jq -r .tag_name) - echo "VERSION=${LATEST}" >> $GITHUB_ENV + base="${BEFORE_SHA}" + head="${GITHUB_SHA_VALUE}" fi + if ! git rev-parse --git-dir >/dev/null 2>&1; then + echo "Git metadata is unavailable; treating the workflow as fully impacted." + echo "build_related=true" >> "${GITHUB_OUTPUT}" + echo "xml_related=true" >> "${GITHUB_OUTPUT}" + echo "renovate_related=true" >> "${GITHUB_OUTPUT}" + exit 0 + fi + + if [[ -z "${base}" || "${base}" =~ ^0+$ ]]; then + if ! changed_files="$(git show --pretty='' --name-only "${head}" 2>/dev/null)"; then + echo "Unable to inspect commit range; treating the workflow as fully impacted." + echo "build_related=true" >> "${GITHUB_OUTPUT}" + echo "xml_related=true" >> "${GITHUB_OUTPUT}" + echo "renovate_related=true" >> "${GITHUB_OUTPUT}" + exit 0 + fi + else + if ! changed_files="$(git diff --name-only "${base}" "${head}" 2>/dev/null)"; then + echo "Unable to diff commit range; treating the workflow as fully impacted." + echo "build_related=true" >> "${GITHUB_OUTPUT}" + echo "xml_related=true" >> "${GITHUB_OUTPUT}" + echo "renovate_related=true" >> "${GITHUB_OUTPUT}" + exit 0 + fi + fi + + printf 'Changed files:\n%s\n' "${changed_files}" + + build_related=false + xml_related=false + renovate_related=false + + while IFS= read -r path; do + [[ -z "${path}" ]] && continue + case "${path}" in + Dockerfile|upstream.toml|rootfs/*|scripts/*) + build_related=true + ;; + simplelogin-aio.xml) + xml_related=true + ;; + renovate.json) + renovate_related=true + ;; + .github/workflows/*) + build_related=true + renovate_related=true + ;; + esac + done <<< "${changed_files}" + + echo "build_related=${build_related}" >> "${GITHUB_OUTPUT}" + echo "xml_related=${xml_related}" >> "${GITHUB_OUTPUT}" + echo "renovate_related=${renovate_related}" >> "${GITHUB_OUTPUT}" + + validate-repo: + needs: detect-changes + if: ${{ needs.detect-changes.outputs.build_related == 'true' || needs.detect-changes.outputs.xml_related == 'true' || needs.detect-changes.outputs.renovate_related == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Validate shell and python scripts + if: ${{ needs.detect-changes.outputs.build_related == 'true' }} + run: | + bash -n scripts/smoke-test.sh + PYTHONPYCACHEPREFIX=/tmp/simplelogin-aio-pyc python3 -m py_compile scripts/check-upstream.py + find rootfs -type f -name '*.sh' -print0 | xargs -0 -n1 bash -n + find rootfs -type f -path '*/run' -print0 | xargs -0 -n1 bash -n + + - name: Validate XML + if: ${{ needs.detect-changes.outputs.build_related == 'true' || needs.detect-changes.outputs.xml_related == 'true' }} + run: | + python3 - <<'PY' + import xml.etree.ElementTree as ET + ET.parse('simplelogin-aio.xml') + print('simplelogin-aio.xml parsed successfully') + PY + + - name: Validate Renovate config + if: ${{ needs.detect-changes.outputs.renovate_related == 'true' }} + run: npx --yes --package renovate renovate-config-validator renovate.json + + pinned-actions: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Enforce pinned action SHAs + run: | + python3 - <<'PY' + import pathlib + import re + import sys + + workflow_dir = pathlib.Path(".github/workflows") + pattern = re.compile(r"^\s*uses:\s*([^@\s]+)@([^\s#]+)") + sha_pattern = re.compile(r"^[0-9a-f]{40}$") + failures = [] + + for path in sorted(workflow_dir.glob("*.yml")): + for lineno, line in enumerate(path.read_text().splitlines(), start=1): + match = pattern.match(line) + if not match: + continue + target, ref = match.groups() + if target.startswith("./"): + continue + if not sha_pattern.fullmatch(ref): + failures.append(f"{path}:{lineno}: action is not pinned to a full SHA -> {line.strip()}") + + if failures: + print("\n".join(failures), file=sys.stderr) + sys.exit(1) + print("All workflow actions are pinned to full commit SHAs.") + PY + + dependency-review: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Dependency review + uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 + + smoke-test: + if: ${{ needs.detect-changes.outputs.build_related == 'true' && (github.event_name != 'workflow_dispatch' || inputs.run_smoke_test == true) }} + needs: + - detect-changes + - validate-repo + - pinned-actions + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - name: Build local test image + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + context: . + platforms: linux/amd64 + load: true + tags: simplelogin-aio:ci + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Smoke test + run: | + chmod +x scripts/smoke-test.sh + export KEEP_SMOKE_ARTIFACTS=1 + bash -x ./scripts/smoke-test.sh simplelogin-aio:ci + + - name: Dump smoke test diagnostics + if: failure() + run: | + docker ps -a + docker inspect simplelogin-aio-smoke || true + docker logs simplelogin-aio-smoke || true + + publish: + if: ${{ needs.detect-changes.outputs.build_related == 'true' && github.event_name != 'pull_request' && ((github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && inputs.publish_image == true)) }} + needs: + - detect-changes + - validate-repo + - pinned-actions + - smoke-test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + - name: Log in to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: - registry: ghcr.io - username: JSONbored + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Compute image tags + id: prep + run: | + image="${REGISTRY}/${IMAGE_NAME}" + sha_tag="${image}:sha-${GITHUB_SHA}" + version="$(sed -n 's/^ARG UPSTREAM_VERSION=//p' Dockerfile | head -n1)" + version_no_v="${version#v}" + + { + echo "upstream_version=${version}" + echo "tags<> "${GITHUB_OUTPUT}" - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . + platforms: linux/amd64 push: true - tags: | - ghcr.io/jsonbored/simplelogin-aio:latest - ghcr.io/jsonbored/simplelogin-aio:sl-${{ env.VERSION }} cache-from: type=gha - cache-to: type=gha,mode=max \ No newline at end of file + cache-to: type=gha,mode=max + tags: ${{ steps.prep.outputs.tags }} + labels: | + org.opencontainers.image.source=https://github.com/JSONbored/simplelogin-aio + org.opencontainers.image.title=simplelogin-aio + org.opencontainers.image.version=${{ steps.prep.outputs.upstream_version || '' }} + io.jsonbored.upstream.name=SimpleLogin + + sync-awesome-unraid: + if: ${{ needs.detect-changes.outputs.xml_related == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main' }} + needs: + - detect-changes + - validate-repo + - pinned-actions + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check sync configuration + env: + SYNC_TOKEN: ${{ secrets.SYNC_TOKEN }} + run: | + if [[ -z "${SYNC_TOKEN}" ]]; then + echo "SYNC_ENABLED=false" >> "${GITHUB_ENV}" + echo "SYNC_TOKEN is not configured; skipping sync." + else + echo "SYNC_ENABLED=true" >> "${GITHUB_ENV}" + fi + + - name: Checkout Source Repository + if: ${{ env.SYNC_ENABLED == 'true' }} + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Checkout Target Repository + if: ${{ env.SYNC_ENABLED == 'true' }} + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + repository: JSONbored/awesome-unraid + token: ${{ secrets.SYNC_TOKEN }} + path: target-repo + + - name: Copy and commit template + if: ${{ env.SYNC_ENABLED == 'true' }} + run: | + cp simplelogin-aio.xml target-repo/simplelogin-aio.xml + cd target-repo + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add simplelogin-aio.xml + git diff --quiet && git diff --staged --quiet || (git commit -m "chore: auto-sync simplelogin-aio.xml from upstream" && git push) diff --git a/.github/workflows/check-upstream.yml b/.github/workflows/check-upstream.yml new file mode 100644 index 0000000..2c72578 --- /dev/null +++ b/.github/workflows/check-upstream.yml @@ -0,0 +1,120 @@ +name: Check Upstream Version + +on: + schedule: + - cron: '23 7 * * 1' + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-upstream: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + + - name: Check upstream + id: upstream + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + chmod +x scripts/check-upstream.py + ./scripts/check-upstream.py + + - name: Stop if already current + if: ${{ steps.upstream.outputs.updates_available != 'true' }} + run: echo "Upstream is already current." + + - name: Create update branch + if: ${{ steps.upstream.outputs.updates_available == 'true' && steps.upstream.outputs.strategy == 'pr' }} + run: git checkout -b "codex/upstream-${{ steps.upstream.outputs.latest_version }}" + + - name: Apply upstream version bump + if: ${{ steps.upstream.outputs.updates_available == 'true' && steps.upstream.outputs.strategy == 'pr' }} + env: + WRITE_UPSTREAM_VERSION: "true" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./scripts/check-upstream.py + + - name: Commit upstream version bump + if: ${{ steps.upstream.outputs.updates_available == 'true' && steps.upstream.outputs.strategy == 'pr' }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Dockerfile + git commit -m "chore: bump upstream to ${{ steps.upstream.outputs.latest_version }}" + + - name: Push update branch + if: ${{ steps.upstream.outputs.updates_available == 'true' && steps.upstream.outputs.strategy == 'pr' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: git push --force --set-upstream origin "codex/upstream-${{ steps.upstream.outputs.latest_version }}" + + - name: Open or update pull request + if: ${{ steps.upstream.outputs.updates_available == 'true' && steps.upstream.outputs.strategy == 'pr' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + body_file="$(mktemp)" + cat > "${body_file}" < "${body_file}" <&2; exit 1 ;; \ + esac && \ + curl -L -o /tmp/s6-overlay-arch.tar.xz "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${s6_arch}.tar.xz" && \ tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && \ - tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz && \ - rm /tmp/s6-overlay-noarch.tar.xz /tmp/s6-overlay-x86_64.tar.xz - -# Ensure base directories exist -RUN mkdir -p /appdata/postgres /appdata/redis /appdata/dkim /appdata/sl /run/postgresql && \ - chown -R postgres:postgres /run/postgresql + tar -C / -Jxpf /tmp/s6-overlay-arch.tar.xz && \ + useradd --system --create-home --home-dir /home/simplelogin --shell /usr/sbin/nologin simplelogin && \ + mkdir -p /appdata/postgres /appdata/redis /appdata/dkim /appdata/sl/upload /pgp /run/postgresql && \ + chown -R postgres:postgres /appdata/postgres /run/postgresql && \ + chown -R redis:redis /appdata/redis && \ + chown -R simplelogin:simplelogin /appdata/sl /pgp && \ + rm -rf /tmp/* /var/lib/apt/lists/* COPY rootfs/ / +RUN find /etc/cont-init.d -type f -exec chmod +x {} \; && \ + find /etc/services.d -type f -name "run" -exec chmod +x {} \; + +EXPOSE 7777 25 +VOLUME ["/appdata", "/pgp"] + +HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=5 \ + CMD curl -fsS http://127.0.0.1:7777/health >/dev/null || exit 1 + ENTRYPOINT ["/init"] diff --git a/README.md b/README.md index b59a98e..8afacab 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,62 @@ -
+# simplelogin-aio -# SimpleLogin AIO (All-in-One) for Unraid +SimpleLogin packaged as a true All-In-One Unraid container. -[![Docker Image Size](https://img.shields.io/docker/image-size/jsonbored/simplelogin-aio/latest?color=blue&label=Image%20Size)](https://github.com/JSONbored/simplelogin-aio/pkgs/container/simplelogin-aio) -[![GitHub License](https://img.shields.io/github/license/JSONbored/simplelogin-aio?color=green)](https://github.com/JSONbored/simplelogin-aio/blob/main/LICENSE) -[![Unraid Community Applications](https://img.shields.io/badge/Unraid-CA%20Template-orange)](https://unraid.net/community/apps) +`simplelogin-aio` bundles the SimpleLogin web app, background jobs, inbound email handler, Postfix, PostgreSQL, and Redis into a single container with Unraid-friendly persistence. A normal install only needs one image, one `/appdata` mapping, and the usual domain + DNS setup required by any self-hosted mail stack. -An ultra-simplified, 100% self-contained deployment of [SimpleLogin](https://simplelogin.io) designed explicitly for Unraid homelabs. +## What This Repo Ships -
+- A single-container `ghcr.io/jsonbored/simplelogin-aio:latest` image +- Explicit image tags matching the pinned upstream release, plus `latest` and `sha-...` +- An Unraid CA template at [simplelogin-aio.xml](/tmp/simplelogin-aio/simplelogin-aio.xml) +- A local smoke test at [scripts/smoke-test.sh](/tmp/simplelogin-aio/scripts/smoke-test.sh) +- Upstream release monitoring via [upstream.toml](/tmp/simplelogin-aio/upstream.toml) and [scripts/check-upstream.py](/tmp/simplelogin-aio/scripts/check-upstream.py) +- Automated `awesome-unraid` sync for the XML ---- +## Included Services -Instead of configuring 3 different templates, managing custom Docker networks, and bootstrapping external PostgreSQL/Redis databases, this image handles the entire stack internals for you. It's designed to provide a "Binhex-style" one-click installation experience for users who just want it to work. +- SimpleLogin web UI on port `7777` +- Postfix for inbound SMTP on port `25` +- Embedded PostgreSQL inside the container +- Embedded Redis inside the container +- SimpleLogin email handler and background job runner -## 📦 What's Inside the "Mega-Container" -This image uses `s6-overlay` to gracefully orchestrate the entire self-hosted alias ecosystem invisibly: -- 🌐 **The Web UI:** SimpleLogin's Python-based dashboard. -- ⚙️ **The Task Runner:** SimpleLogin Background Job Worker (Celery). -- 📧 **The MTA Router:** **Postfix** is built-in to route inbound/outbound mail instantly. -- 🗄️ **The Database:** **PostgreSQL 14** is auto-provisioned securely internally. -- ⚡ **The Cache:** **Redis** is auto-provisioned for rapid background queuing. +## Important Runtime Notes -## 🚀 Installation (For Beginners) -1. Add this repository to your Unraid Community Applications: `https://github.com/JSONbored/simplelogin-aio` -2. Search and Install **SimpleLogin-AIO**. -3. Fill out your `App URL` and `Email Domain`. -4. Pick an **SMTP Relay Provider** from the dropdown (to bypass residential Port 25 outbound blocking) and enter your credentials. -5. Click **Apply**. +- Current upstream SimpleLogin container support is `linux/amd64` only, so `simplelogin-aio` currently publishes amd64-only images. +- First boot initializes PostgreSQL, writes the runtime `.env`, applies `alembic upgrade head`, and then runs `init_app.py`. +- `/appdata` is the main persistent volume. `/pgp` is optional for advanced PGP usage. +- If you already run external PostgreSQL or Redis, set `DB_URI` and `REDIS_URL` in the Unraid template and the internal daemons will stay idle. -**That's it.** The container will silently generate a secure internal database, apply migrations, build your DKIM cryptography keys, and map everything persistently to your array under a single `/mnt/user/appdata/simplelogin-aio` folder. +## Quick Start -## 🛠️ Power Users (External Databases) -If you already run a shared `postgres` or `redis` container on your Unraid box and don't want the overhead of the internal versions running, you can easily disable them! +1. Install the Unraid template. +2. Set `URL`, `EMAIL_DOMAIN`, `SUPPORT_EMAIL`, and `FLASK_SECRET`. +3. Pick a relay mode if your ISP blocks outbound port `25`. +4. Forward inbound TCP port `25` to the Unraid host if you want aliases to receive internet mail. +5. Review [docs/simplelogin-setup.md](/tmp/simplelogin-aio/docs/simplelogin-setup.md) for the required DNS records and mail-routing checklist. -Inside the Unraid Template, toggle the **Advanced View**. -- Fill out the `Advanced: External DB_URI` variable with your remote Postgres string. -- Fill out the `Advanced: External Redis URL` variable. +## Validation -If the initialization script detects those variables on startup, it will **completely skip** booting the internal PostgreSQL/Redis daemons and route traffic externally. +Local validation completed on March 29, 2026: -## 🛡️ Advanced Features (Self-Hosted Maximalists) -This template exposes advanced variables to integrate with your existing ecosystem. Toggle "Advanced View" in Unraid to configure: -- **SSO / Identity:** Integrate directly with [Tailscale IDP (`tsidp`)](https://tailscale.com), Authelia, or Authentik via the generic OIDC variables. -- **Anti-Spam:** Offload scanning to a separate `SpamAssassin` container by linking its IP. -- **Telemetry:** Point error tracking and analytics to your own self-hosted `Sentry` and `Plausible` instances. -- **PGP Encryption:** Mount your own keyring to encrypt emails locally before they leave the server. +- explicit `linux/amd64` Docker build succeeded +- full local smoke test passed end-to-end +- restart and persistence coverage added to the smoke test +- internal PostgreSQL, Redis, web, background jobs, and Postfix all validated in the same container +- workflow hardening added with pinned action SHAs, dependency review, and upstream release tracking -For a full breakdown of these capabilities, read the [Advanced Features Documentation](docs/ADVANCED-FEATURES-RESEARCH.md). +## Support -## 📚 Documentation & Setup -- [Full Setup & DNS Configuration Guide](docs/simplelogin-setup.md) +- Issues: [JSONbored/simplelogin-aio issues](https://github.com/JSONbored/simplelogin-aio/issues) +- Upstream app: [simple-login/app](https://github.com/simple-login/app) ---- +## Funding -
+If this work saves you time, support it here: -### Star History -[![Star History Chart](https://api.star-history.com/svg?repos=JSONbored/simplelogin-aio&type=Date)](https://star-history.com/#JSONbored/simplelogin-aio&Date) +- [GitHub Sponsors](https://github.com/sponsors/JSONbored) -
+## Star History ---- - -## 👨‍💻 About the Creator - -Built with 🖤 by **[JSONbored](https://github.com/JSONbored)**. - -- 🌐 **Portfolio & Services:** [aethereal.dev](https://aethereal.dev) -- 📅 **Book a Call:** [cal.com/aethereal](https://cal.com/aethereal) -- ☕ **Support my work:** [Sponsor on GitHub](https://github.com/sponsors/JSONbored) +[![Star History Chart](https://api.star-history.com/svg?repos=JSONbored/simplelogin-aio&theme=dark)](https://star-history.com/#JSONbored/simplelogin-aio&Date) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 3b423c9..567e544 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,23 +1,9 @@ -# SimpleLogin-AIO Release Notes +# Release Notes -**Repository:** `JSONbored/simplelogin-aio` & `JSONbored/unraid-templates` +## 2026-03-29 -## Overview -SimpleLogin is an open-source email alias service to protect your real email address. This release packages the official `simplelogin/app-ci` image with s6-overlay v3.1.6.2 to run the Web UI (Gunicorn), internal Email Handler (`email_handler.py`), and Background Jobs (`job_runner.py`) as a single, easily deployable All-in-One Unraid container. - -## Requirements -- `postgres-shared:5432` (or any PostgreSQL instance) -- `redis-shared:6380` (or any Redis instance) -- `SimpleLogin-Postfix` (MTA companion container to route inbound/outbound mail) - -## Installation Order -1. Deploy PostgreSQL and Redis containers if not already running. -2. Deploy the `SimpleLogin-Postfix` container (ensure port 25 is mapped or a Relay Mode is configured). -3. Deploy the `SimpleLogin-AIO` container, linking it to the Database, Redis, and Postfix server. -4. Review the container logs for your auto-generated DKIM DNS TXT record and setup your domain's A, MX, SPF, DMARC, and DKIM records. - -## Initial Release (2026-03-21) -- **Initial Release:** Created the complete Unraid Community Application XML template with 70+ natively exposed environment variables. -- **DKIM Generation:** Implemented automatic 1024-bit RSA DKIM key generation on first boot. -- **Relay Support:** `SimpleLogin-Postfix` fully configured to bypass ISP Port 25 blocking with ProtonMail, Brevo, Gmail, and Mailgun relay modes. -- **Idempotency:** Robust startup scripts ensuring safe database migrations and configuration bridging on every restart. \ No newline at end of file +- Rebuilt `simplelogin-aio` around the official `simplelogin/app-ci` upstream release `v4.79.0` +- Embedded PostgreSQL, Redis, Postfix, SimpleLogin web, email handler, and background jobs into one supervised AIO container +- Added a real local smoke test with restart and persistence coverage +- Added upstream release tracking, pinned GitHub Actions SHAs, and dependency-review security checks +- Kept the Unraid install model focused on a single `/appdata` path plus optional `/pgp` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f2b717d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,20 @@ +# Security Policy + +## Supported Versions + +Only the current `latest` tag, the current upstream-matching version tags, and the `main` branch are supported with security fixes. + +| Version | Supported | +| ------- | --------- | +| latest | yes | +| current upstream version tags | yes | +| older | no | + +## Reporting a Vulnerability + +Please report security issues privately instead of opening a public issue. + +- Preferred: GitHub private vulnerability reporting for this repository +- Fallback: `security@aethereal.dev` + +Include the affected image tag, reproduction details, impact, and any mitigations you already verified. diff --git a/build-status.md b/build-status.md new file mode 100644 index 0000000..c7dde94 --- /dev/null +++ b/build-status.md @@ -0,0 +1,3 @@ +# Build Status + +Local Docker smoke tests passed on 2026-03-29 for `linux/amd64`, including restart and persistence verification, after hardening the image build, workflow security, and upstream release tracking. diff --git a/docs/simplelogin-setup.md b/docs/simplelogin-setup.md index c8e059a..55f3ebc 100644 --- a/docs/simplelogin-setup.md +++ b/docs/simplelogin-setup.md @@ -1,30 +1,75 @@ -# SimpleLogin AIO: Unraid Setup Guide +# SimpleLogin AIO Setup Guide -This guide covers setting up the "Mega-Container" version of SimpleLogin on Unraid, which handles PostgreSQL, Redis, and Postfix internally. +This guide covers the minimum pieces required to make `simplelogin-aio` work on Unraid. -## 1. Cloudflare DNS Setup -To route mail to your home internet, you need to point DNS records to your WAN IP. +## 1. Pick Your Hostnames -1. Create an `A` record pointing `mail.yourdomain.com` to your public Unraid IP. -2. Create an `MX` record on `yourdomain.com` pointing to `mail.yourdomain.com` with priority `10`. -3. Create a `TXT` record on `yourdomain.com` for SPF: `v=spf1 mx ~all`. -4. Create a `TXT` record for DMARC on `_dmarc.yourdomain.com`: `v=DMARC1; p=quarantine; rua=mailto:admin@yourdomain.com`. +You normally want: -## 2. Unraid Network Setup (Port Forwarding) -1. In your physical hardware router (e.g., pfSense, UniFi, Eero), forward **TCP Port 25** to your Unraid Server's local IP address. -2. Without this, the internet cannot deliver mail to your aliases! +- `app.example.com` for the web UI +- `mail.example.com` for inbound SMTP delivery -## 3. Install the Template -1. Search CA for `SimpleLogin-AIO`. -2. Fill out your `App URL` (e.g., `https://app.yourdomain.com`), `Email Domain`, and `Support Email`. -3. Select your `SMTP Relay Mode` (crucial if your ISP blocks outbound Port 25, which 99% of them do). Fill in the credentials if using Brevo, Gmail, etc. -4. Click Apply. +Set the Unraid template like this: -## 4. Retrieve DKIM Keys -The container takes about 30-45 seconds on its first boot to generate the internal database and cryptographic keys. -1. Open the Docker Logs for SimpleLogin-AIO. -2. Scroll until you see the `SUCCESS: DKIM Keys Generated` banner. -3. Copy the raw `TXT` record provided in the logs. -4. Go back to Cloudflare and add that `TXT` record (Name: `dkim._domainkey`). +- `URL=https://app.example.com` +- `EMAIL_DOMAIN=example.com` +- `SUPPORT_EMAIL=support@example.com` -You are fully self-hosted! +## 2. Add DNS Records + +At minimum, add: + +- `A` or `AAAA` record for `app.example.com` +- `A` or `AAAA` record for `mail.example.com` +- `MX` record for `example.com` pointing to `mail.example.com` +- SPF TXT record on `example.com` +- DMARC TXT record on `_dmarc.example.com` + +SimpleLogin will also need DKIM once the container has booted and generated its keys. + +## 3. Forward Mail Traffic + +Inbound internet mail must reach the Unraid host: + +- forward TCP port `25` from your router/firewall to the Unraid server + +If your ISP blocks outbound port `25`, choose a relay provider in the template: + +- `brevo` +- `protonmail` +- `gmail` +- `mailgun` +- `custom` + +If your ISP does not block outbound mail, `direct` can work. + +## 4. Start the Container + +On first boot the container will: + +- initialize PostgreSQL if `DB_URI` is not set +- start Redis if `REDIS_URL` is not set +- write the runtime `.env` +- configure Postfix +- apply `alembic upgrade head` +- run `init_app.py` once + +The first start can take longer than a normal restart because the internal database is being prepared. + +## 5. Confirm It Is Healthy + +After the container comes up: + +- open the web UI on port `7777` +- confirm `/health` responds +- check the logs for any Postfix relay or DNS warnings +- check `/appdata/sl` and `/appdata/postgres` were populated + +## 6. Advanced Overrides + +You can point the container at external services: + +- set `DB_URI` to skip the internal PostgreSQL daemon +- set `REDIS_URL` to skip the internal Redis daemon + +This keeps the Unraid template flexible without forcing beginners into a multi-container setup. diff --git a/renovate.json b/renovate.json index 5db72dd..310fbe4 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,26 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:recommended" + "config:recommended", + "helpers:pinGitHubActionDigests", + ":dependencyDashboard" + ], + "pinDigests": true, + "labels": ["dependencies"], + "packageRules": [ + { + "matchManagers": ["github-actions", "dockerfile"], + "groupName": "non-major infrastructure updates", + "matchUpdateTypes": ["minor", "patch", "digest"], + "matchCurrentVersion": "!/^0/" + }, + { + "matchManagers": ["github-actions"], + "addLabels": ["github-actions"] + }, + { + "matchManagers": ["dockerfile"], + "addLabels": ["docker"] + } ] } diff --git a/rootfs/etc/cont-init.d/01-bootstrap-databases.sh b/rootfs/etc/cont-init.d/01-bootstrap-databases.sh old mode 100755 new mode 100644 index 81eaf96..7f76cd0 --- a/rootfs/etc/cont-init.d/01-bootstrap-databases.sh +++ b/rootfs/etc/cont-init.d/01-bootstrap-databases.sh @@ -1,42 +1,51 @@ #!/command/with-contenv bash +set -euo pipefail echo "Checking internal database requirements..." -mkdir -p /appdata/postgres /appdata/redis /appdata/dkim /appdata/sl +mkdir -p /appdata/postgres /appdata/redis /appdata/dkim /appdata/sl/upload /pgp /run/postgresql chown -R redis:redis /appdata/redis -chown -R postgres:postgres /appdata/postgres +chown -R postgres:postgres /appdata/postgres /run/postgresql +chown -R simplelogin:simplelogin /appdata/sl /pgp +chmod 700 /pgp -# Auto-provision Postgres if DB_URI is empty -if [ -z "$DB_URI" ]; then +rm -rf /code/static/upload +ln -sfn /appdata/sl/upload /code/static/upload + +DB_URI_VALUE="${DB_URI:-}" +if [ -z "$DB_URI_VALUE" ]; then echo "No external DB_URI provided. Bootstrapping internal PostgreSQL..." - - # Initialize DB if empty + if [ ! -f "/appdata/postgres/PG_VERSION" ]; then echo "Initializing bare Postgres cluster..." - # Get postgres version directory (e.g. /usr/lib/postgresql/14/bin/initdb) PG_BIN_DIR=$(ls -d /usr/lib/postgresql/*/bin | head -n 1) - su - postgres -c "$PG_BIN_DIR/initdb -D /appdata/postgres" - - # We need a random password for the simplelogin user to secure it internally - INTERNAL_PG_PASS=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) - echo "$INTERNAL_PG_PASS" > /appdata/postgres/.sl_internal_pass + su -s /bin/bash postgres -c "$PG_BIN_DIR/initdb -D /appdata/postgres" + + INTERNAL_PG_PASS=$(openssl rand -hex 24) + printf '%s\n' "$INTERNAL_PG_PASS" > /appdata/postgres/.sl_internal_pass chown postgres:postgres /appdata/postgres/.sl_internal_pass - - # Start PG temporarily to create user/db - su - postgres -c "$PG_BIN_DIR/pg_ctl -D /appdata/postgres start" - sleep 3 - su - postgres -c "psql -c \"CREATE USER simplelogin WITH PASSWORD '$INTERNAL_PG_PASS';\"" - su - postgres -c "psql -c \"CREATE DATABASE simplelogin OWNER simplelogin;\"" - su - postgres -c "$PG_BIN_DIR/pg_ctl -D /appdata/postgres stop" + + su -s /bin/bash postgres -c "$PG_BIN_DIR/pg_ctl -D /appdata/postgres -o \"-c listen_addresses='127.0.0.1'\" -w start" + su -s /bin/bash postgres -c "psql postgres <<'SQL' +DO \$\$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'simplelogin') THEN + CREATE ROLE simplelogin LOGIN PASSWORD '${INTERNAL_PG_PASS}'; + ELSE + ALTER ROLE simplelogin WITH PASSWORD '${INTERNAL_PG_PASS}'; + END IF; +END +\$\$; +SQL" + su -s /bin/bash postgres -c "psql postgres -tAc \"SELECT 1 FROM pg_database WHERE datname='simplelogin'\" | grep -qx 1 || createdb -O simplelogin simplelogin" + su -s /bin/bash postgres -c "$PG_BIN_DIR/pg_ctl -D /appdata/postgres -m fast -w stop" fi - - # Export the DB_URI for the python app to use downstream + INTERNAL_PG_PASS=$(cat /appdata/postgres/.sl_internal_pass) - # Write to s6-env so other scripts see it - echo "postgresql://simplelogin:$INTERNAL_PG_PASS@127.0.0.1:5432/simplelogin" > /var/run/s6/container_environment/DB_URI + DB_URI_VALUE="postgresql://simplelogin:${INTERNAL_PG_PASS}@127.0.0.1:5432/simplelogin" fi -if [ -z "$REDIS_URL" ]; then - echo "redis://127.0.0.1:6379/0" > /var/run/s6/container_environment/REDIS_URL -fi +REDIS_URL_VALUE="${REDIS_URL:-redis://127.0.0.1:6379/0}" +printf '%s\n' "$DB_URI_VALUE" > /var/run/s6/container_environment/DB_URI +printf '%s\n' "$REDIS_URL_VALUE" > /var/run/s6/container_environment/REDIS_URL diff --git a/rootfs/etc/cont-init.d/01-validate-env.sh b/rootfs/etc/cont-init.d/01-validate-env.sh index f9bec12..2ebc662 100644 --- a/rootfs/etc/cont-init.d/01-validate-env.sh +++ b/rootfs/etc/cont-init.d/01-validate-env.sh @@ -1,77 +1,70 @@ #!/command/with-contenv bash -# ============================================================================== -# 01-validate-env.sh -# Purpose: Validates all required environment variables for SimpleLogin-AIO. -# Fails fast and prevents the container from starting if critical configs are missing. -# ============================================================================== +set -euo pipefail -# ANSI Color Codes for readability RED='\033[0;31m' YELLOW='\033[1;33m' GREEN='\033[0;32m' -NC='\033[0m' # No Color +NC='\033[0m' echo -e "${GREEN}Starting SimpleLogin-AIO environment validation...${NC}" ERROR_COUNT=0 -# Check URL -if [ -z "$URL" ]; then - echo -e "${RED}[ERROR] URL is not set.${NC} This is the public-facing URL of your SimpleLogin instance (e.g., https://app.yourdomain.com). Please set the 'App URL' variable." - ERROR_COUNT=$((ERROR_COUNT+1)) -fi - -# Check EMAIL_DOMAIN -if [ -z "$EMAIL_DOMAIN" ]; then - echo -e "${RED}[ERROR] EMAIL_DOMAIN is not set.${NC} This is the primary domain used to create aliases (e.g., yourdomain.com). Please set the 'Email Domain' variable." - ERROR_COUNT=$((ERROR_COUNT+1)) -fi - -# Check SUPPORT_EMAIL -if [ -z "$SUPPORT_EMAIL" ]; then - echo -e "${RED}[ERROR] SUPPORT_EMAIL is not set.${NC} This is the address from which transactional system emails are sent. Please set the 'Support Email' variable." - ERROR_COUNT=$((ERROR_COUNT+1)) -fi +for required_var in URL EMAIL_DOMAIN SUPPORT_EMAIL FLASK_SECRET; do + if [ -z "${!required_var:-}" ]; then + echo -e "${RED}[ERROR] ${required_var} is not set.${NC}" + ERROR_COUNT=$((ERROR_COUNT + 1)) + fi +done -# Check FLASK_SECRET -if [ -z "$FLASK_SECRET" ]; then - echo -e "${RED}[ERROR] FLASK_SECRET is not set.${NC} This is the cryptographic secret used by Flask to sign session cookies. Please set the 'Flask Secret' variable to a random string." - ERROR_COUNT=$((ERROR_COUNT+1)) -fi - -# Check DB_URI -if [ -z "$DB_URI" ]; then - echo -e "${RED}[ERROR] DB_URI is not set.${NC} This is the connection string for the PostgreSQL database. Please set the 'Database URI' variable." - ERROR_COUNT=$((ERROR_COUNT+1)) -fi - -# Check POSTFIX_SERVER -if [ -z "$POSTFIX_SERVER" ]; then - echo -e "${RED}[ERROR] POSTFIX_SERVER is not set.${NC} This is the hostname or IP of the MTA (Postfix) server. Please set the 'Postfix Server' variable." - ERROR_COUNT=$((ERROR_COUNT+1)) -fi +RELAY_MODE_VALUE="${RELAY_MODE:-direct}" +case "$RELAY_MODE_VALUE" in + direct) + ;; + brevo) + [ -n "${BREVO_USERNAME:-}" ] || { echo -e "${RED}[ERROR] BREVO_USERNAME is required when RELAY_MODE=brevo.${NC}"; ERROR_COUNT=$((ERROR_COUNT + 1)); } + [ -n "${BREVO_PASSWORD:-}" ] || { echo -e "${RED}[ERROR] BREVO_PASSWORD is required when RELAY_MODE=brevo.${NC}"; ERROR_COUNT=$((ERROR_COUNT + 1)); } + ;; + protonmail) + [ -n "${PROTONMAIL_TOKEN:-}" ] || { echo -e "${RED}[ERROR] PROTONMAIL_TOKEN is required when RELAY_MODE=protonmail.${NC}"; ERROR_COUNT=$((ERROR_COUNT + 1)); } + ;; + gmail) + [ -n "${GMAIL_USERNAME:-}" ] || { echo -e "${RED}[ERROR] GMAIL_USERNAME is required when RELAY_MODE=gmail.${NC}"; ERROR_COUNT=$((ERROR_COUNT + 1)); } + [ -n "${GMAIL_APP_PASSWORD:-}" ] || { echo -e "${RED}[ERROR] GMAIL_APP_PASSWORD is required when RELAY_MODE=gmail.${NC}"; ERROR_COUNT=$((ERROR_COUNT + 1)); } + ;; + mailgun) + [ -n "${MAILGUN_USERNAME:-}" ] || { echo -e "${RED}[ERROR] MAILGUN_USERNAME is required when RELAY_MODE=mailgun.${NC}"; ERROR_COUNT=$((ERROR_COUNT + 1)); } + [ -n "${MAILGUN_PASSWORD:-}" ] || { echo -e "${RED}[ERROR] MAILGUN_PASSWORD is required when RELAY_MODE=mailgun.${NC}"; ERROR_COUNT=$((ERROR_COUNT + 1)); } + ;; + custom) + [ -n "${CUSTOM_RELAYHOST:-}" ] || { echo -e "${RED}[ERROR] CUSTOM_RELAYHOST is required when RELAY_MODE=custom.${NC}"; ERROR_COUNT=$((ERROR_COUNT + 1)); } + [ -n "${CUSTOM_USERNAME:-}" ] || { echo -e "${RED}[ERROR] CUSTOM_USERNAME is required when RELAY_MODE=custom.${NC}"; ERROR_COUNT=$((ERROR_COUNT + 1)); } + [ -n "${CUSTOM_PASSWORD:-}" ] || { echo -e "${RED}[ERROR] CUSTOM_PASSWORD is required when RELAY_MODE=custom.${NC}"; ERROR_COUNT=$((ERROR_COUNT + 1)); } + ;; + *) + echo -e "${RED}[ERROR] RELAY_MODE=${RELAY_MODE_VALUE} is not supported.${NC}" + ERROR_COUNT=$((ERROR_COUNT + 1)) + ;; +esac -# Halt if there are errors if [ "$ERROR_COUNT" -gt 0 ]; then echo -e "${RED}Validation failed with $ERROR_COUNT error(s). Container startup halted.${NC}" exit 1 fi -# Check important optional variables and print warnings -if [ -z "$ADMIN_EMAIL" ] && [ -z "$SL_ADMIN_EMAIL" ]; then +if [ -z "${ADMIN_EMAIL:-}" ] && [ -z "${SL_ADMIN_EMAIL:-}" ]; then echo -e "${YELLOW}[WARNING] ADMIN_EMAIL is not set.${NC} You will not receive general system statistics and alerts." fi -if [ -z "$DISABLE_REGISTRATION" ]; then - echo -e "${YELLOW}[WARNING] DISABLE_REGISTRATION is not set.${NC} Your instance currently allows open registration. It is highly recommended to set this to 1 after creating your admin account to prevent abuse." +if [ -z "${DISABLE_REGISTRATION:-}" ]; then + echo -e "${YELLOW}[WARNING] DISABLE_REGISTRATION is not set.${NC} Your instance currently allows open registration." fi -# Print confirmed values for non-sensitive vars echo "========================================" echo "Confirmed Environment Configuration:" echo "URL: $URL" echo "EMAIL_DOMAIN: $EMAIL_DOMAIN" echo "SUPPORT_EMAIL: $SUPPORT_EMAIL" -echo "POSTFIX_SERVER: $POSTFIX_SERVER" +echo "RELAY_MODE: $RELAY_MODE_VALUE" echo "========================================" echo -e "${GREEN}Environment validation passed successfully.${NC}" diff --git a/rootfs/etc/cont-init.d/03-write-env.sh b/rootfs/etc/cont-init.d/03-write-env.sh index 5082aa2..039a8a8 100644 --- a/rootfs/etc/cont-init.d/03-write-env.sh +++ b/rootfs/etc/cont-init.d/03-write-env.sh @@ -1,13 +1,6 @@ #!/command/with-contenv bash -# ============================================================================== -# 03-write-env.sh -# Purpose: Translates Unraid XML environment variables into the specific format -# that SimpleLogin expects in its /code/.env file. This isolates host configuration -# from application expectations and ensures every variable is mapped. -# Regenerates on every container start. -# ============================================================================== +set -euo pipefail -# ANSI Color Codes CYAN='\033[1;36m' GREEN='\033[0;32m' NC='\033[0m' @@ -16,141 +9,68 @@ ENV_FILE="/code/.env" echo -e "${CYAN}Generating SimpleLogin .env configuration...${NC}" -# Clear existing file -> "$ENV_FILE" - -# ------------------------------------------------------------------------------ -# CORE & REQUIRED VARIABLES -# ------------------------------------------------------------------------------ -echo "URL=${URL}" >> "$ENV_FILE" -echo "EMAIL_DOMAIN=${EMAIL_DOMAIN}" >> "$ENV_FILE" -echo "SUPPORT_EMAIL=${SUPPORT_EMAIL}" >> "$ENV_FILE" -echo "FLASK_SECRET=${FLASK_SECRET}" >> "$ENV_FILE" -echo "DB_URI=${DB_URI}" >> "$ENV_FILE" -echo "REDIS_URL=${REDIS_URL}" >> "$ENV_FILE" -echo "POSTFIX_SERVER=127.0.0.1" >> "$ENV_FILE" +DB_URI_VALUE="${DB_URI:-$(cat /var/run/s6/container_environment/DB_URI 2>/dev/null || true)}" +REDIS_URL_VALUE="${REDIS_URL:-$(cat /var/run/s6/container_environment/REDIS_URL 2>/dev/null || true)}" +EMAIL_SERVERS_VALUE="${EMAIL_SERVERS_WITH_PRIORITY:-[(10, \"mail.${EMAIL_DOMAIN}.\")]}" +PARTNER_API_TOKEN_SECRET_VALUE="${PARTNER_API_TOKEN_SECRET:-${FLASK_SECRET}}" -# ------------------------------------------------------------------------------ -# DEFAULTS & HARDCODED PATHS -# ------------------------------------------------------------------------------ -echo "POSTFIX_PORT=${POSTFIX_PORT:-25}" >> "$ENV_FILE" -echo "DKIM_PRIVATE_KEY_PATH=/dkim.key" >> "$ENV_FILE" -echo "DKIM_PUBLIC_KEY_PATH=/dkim.pub.key" >> "$ENV_FILE" -echo "GNUPGHOME=/pgp" >> "$ENV_FILE" - -# Self-hosting optimized defaults -echo "LOCAL_FILE_UPLOAD=${LOCAL_FILE_UPLOAD:-true}" >> "$ENV_FILE" -echo "DISABLE_ALIAS_SUFFIX=${DISABLE_ALIAS_SUFFIX:-1}" >> "$ENV_FILE" -echo "DISABLE_REGISTRATION=${DISABLE_REGISTRATION:-0}" >> "$ENV_FILE" -echo "DISABLE_ONBOARDING=${DISABLE_ONBOARDING:-true}" >> "$ENV_FILE" -echo "MAX_NB_EMAIL_FREE_PLAN=${MAX_NB_EMAIL_FREE_PLAN:-10}" >> "$ENV_FILE" -echo "ENFORCE_SPF=${ENFORCE_SPF:-true}" >> "$ENV_FILE" +> "$ENV_FILE" -# Support both SL_NAMESERVERS and NAMESERVERS syntax -if [ -n "$SL_NAMESERVERS" ]; then +cat >>"$ENV_FILE" <> "$ENV_FILE" -elif [ -n "$NAMESERVERS" ]; then +elif [ -n "${NAMESERVERS:-}" ]; then echo "NAMESERVERS=${NAMESERVERS}" >> "$ENV_FILE" else echo "NAMESERVERS=1.1.1.1,1.0.0.1" >> "$ENV_FILE" fi -# ------------------------------------------------------------------------------ -# OPTIONAL VARIABLES (Appended only if set) -# ------------------------------------------------------------------------------ - -# General Settings -[ -n "$WORDS_FILE_PATH" ] && echo "WORDS_FILE_PATH=${WORDS_FILE_PATH}" >> "$ENV_FILE" -[ -n "$TEMP_DIR" ] && echo "TEMP_DIR=${TEMP_DIR}" >> "$ENV_FILE" -[ -n "$COLOR_LOG" ] && echo "COLOR_LOG=${COLOR_LOG}" >> "$ENV_FILE" -[ -n "$NOT_SEND_EMAIL" ] && echo "NOT_SEND_EMAIL=${NOT_SEND_EMAIL}" >> "$ENV_FILE" -[ -n "$LANDING_PAGE_URL" ] && echo "LANDING_PAGE_URL=${LANDING_PAGE_URL}" >> "$ENV_FILE" -[ -n "$STATUS_PAGE_URL" ] && echo "STATUS_PAGE_URL=${STATUS_PAGE_URL}" >> "$ENV_FILE" -[ -n "$PARTNER_API_TOKEN_SECRET" ] && echo "PARTNER_API_TOKEN_SECRET=${PARTNER_API_TOKEN_SECRET}" >> "$ENV_FILE" -[ -n "$ALLOWED_REDIRECT_DOMAINS" ] && echo "ALLOWED_REDIRECT_DOMAINS=${ALLOWED_REDIRECT_DOMAINS}" >> "$ENV_FILE" - -# Email Routing & Domains -[ -n "$OTHER_ALIAS_DOMAINS" ] && echo "OTHER_ALIAS_DOMAINS=${OTHER_ALIAS_DOMAINS}" >> "$ENV_FILE" -[ -n "$ALIAS_DOMAINS" ] && echo "ALIAS_DOMAINS=${ALIAS_DOMAINS}" >> "$ENV_FILE" -[ -n "$PREMIUM_ALIAS_DOMAINS" ] && echo "PREMIUM_ALIAS_DOMAINS=${PREMIUM_ALIAS_DOMAINS}" >> "$ENV_FILE" -[ -n "$FIRST_ALIAS_DOMAIN" ] && echo "FIRST_ALIAS_DOMAIN=${FIRST_ALIAS_DOMAIN}" >> "$ENV_FILE" -[ -n "$SUPPORT_NAME" ] && echo "SUPPORT_NAME=${SUPPORT_NAME}" >> "$ENV_FILE" -[ -n "$ADMIN_EMAIL" ] && echo "ADMIN_EMAIL=${ADMIN_EMAIL}" >> "$ENV_FILE" -[ -n "$EMAIL_SERVERS_WITH_PRIORITY" ] && echo "EMAIL_SERVERS_WITH_PRIORITY=${EMAIL_SERVERS_WITH_PRIORITY}" >> "$ENV_FILE" -[ -n "$POSTMASTER" ] && echo "POSTMASTER=${POSTMASTER}" >> "$ENV_FILE" - -# Security & Limits -[ -n "$PGP_SENDER_PRIVATE_KEY_PATH" ] && echo "PGP_SENDER_PRIVATE_KEY_PATH=${PGP_SENDER_PRIVATE_KEY_PATH}" >> "$ENV_FILE" -[ -n "$OPENID_PRIVATE_KEY_PATH" ] && echo "OPENID_PRIVATE_KEY_PATH=${OPENID_PRIVATE_KEY_PATH}" >> "$ENV_FILE" -[ -n "$OPENID_PUBLIC_KEY_PATH" ] && echo "OPENID_PUBLIC_KEY_PATH=${OPENID_PUBLIC_KEY_PATH}" >> "$ENV_FILE" -[ -n "$ALIAS_LIMIT" ] && echo "ALIAS_LIMIT=${ALIAS_LIMIT}" >> "$ENV_FILE" -[ -n "$ALIAS_AUTOMATIC_DISABLE" ] && echo "ALIAS_AUTOMATIC_DISABLE=${ALIAS_AUTOMATIC_DISABLE}" >> "$ENV_FILE" - -# Bounce Processing (VERP) -[ -n "$BOUNCE_PREFIX" ] && echo "BOUNCE_PREFIX=${BOUNCE_PREFIX}" >> "$ENV_FILE" -[ -n "$BOUNCE_SUFFIX" ] && echo "BOUNCE_SUFFIX=${BOUNCE_SUFFIX}" >> "$ENV_FILE" -[ -n "$BOUNCE_PREFIX_FOR_REPLY_PHASE" ] && echo "BOUNCE_PREFIX_FOR_REPLY_PHASE=${BOUNCE_PREFIX_FOR_REPLY_PHASE}" >> "$ENV_FILE" - -# OAuth & Logins -[ -n "$GITHUB_CLIENT_ID" ] && echo "GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}" >> "$ENV_FILE" -[ -n "$GITHUB_CLIENT_SECRET" ] && echo "GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}" >> "$ENV_FILE" -[ -n "$GOOGLE_CLIENT_ID" ] && echo "GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}" >> "$ENV_FILE" -[ -n "$GOOGLE_CLIENT_SECRET" ] && echo "GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}" >> "$ENV_FILE" -[ -n "$FACEBOOK_CLIENT_ID" ] && echo "FACEBOOK_CLIENT_ID=${FACEBOOK_CLIENT_ID}" >> "$ENV_FILE" -[ -n "$FACEBOOK_CLIENT_SECRET" ] && echo "FACEBOOK_CLIENT_SECRET=${FACEBOOK_CLIENT_SECRET}" >> "$ENV_FILE" -[ -n "$PROTON_CLIENT_ID" ] && echo "PROTON_CLIENT_ID=${PROTON_CLIENT_ID}" >> "$ENV_FILE" -[ -n "$PROTON_CLIENT_SECRET" ] && echo "PROTON_CLIENT_SECRET=${PROTON_CLIENT_SECRET}" >> "$ENV_FILE" -[ -n "$PROTON_BASE_URL" ] && echo "PROTON_BASE_URL=${PROTON_BASE_URL}" >> "$ENV_FILE" -[ -n "$PROTON_VALIDATE_CERTS" ] && echo "PROTON_VALIDATE_CERTS=${PROTON_VALIDATE_CERTS}" >> "$ENV_FILE" -[ -n "$CONNECT_WITH_PROTON" ] && echo "CONNECT_WITH_PROTON=${CONNECT_WITH_PROTON}" >> "$ENV_FILE" -[ -n "$CONNECT_WITH_PROTON_COOKIE_NAME" ] && echo "CONNECT_WITH_PROTON_COOKIE_NAME=${CONNECT_WITH_PROTON_COOKIE_NAME}" >> "$ENV_FILE" - -# Generic OIDC -[ -n "$CONNECT_WITH_OIDC_ICON" ] && echo "CONNECT_WITH_OIDC_ICON=${CONNECT_WITH_OIDC_ICON}" >> "$ENV_FILE" -[ -n "$OIDC_WELL_KNOWN_URL" ] && echo "OIDC_WELL_KNOWN_URL=${OIDC_WELL_KNOWN_URL}" >> "$ENV_FILE" -[ -n "$OIDC_SCOPES" ] && echo "OIDC_SCOPES=${OIDC_SCOPES}" >> "$ENV_FILE" -[ -n "$OIDC_NAME_FIELD" ] && echo "OIDC_NAME_FIELD=${OIDC_NAME_FIELD}" >> "$ENV_FILE" -[ -n "$OIDC_CLIENT_ID" ] && echo "OIDC_CLIENT_ID=${OIDC_CLIENT_ID}" >> "$ENV_FILE" -[ -n "$OIDC_CLIENT_SECRET" ] && echo "OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}" >> "$ENV_FILE" -[ -n "$APPLE_API_SECRET" ] && echo "APPLE_API_SECRET=${APPLE_API_SECRET}" >> "$ENV_FILE" -[ -n "$MACAPP_APPLE_API_SECRET" ] && echo "MACAPP_APPLE_API_SECRET=${MACAPP_APPLE_API_SECRET}" >> "$ENV_FILE" - -# Spam Scanning -[ -n "$ENABLE_SPAM_ASSASSIN" ] && echo "ENABLE_SPAM_ASSASSIN=${ENABLE_SPAM_ASSASSIN}" >> "$ENV_FILE" -[ -n "$SPAMASSASSIN_HOST" ] && echo "SPAMASSASSIN_HOST=${SPAMASSASSIN_HOST}" >> "$ENV_FILE" - -# Analytics & Integrations -[ -n "$HCAPTCHA_SECRET" ] && echo "HCAPTCHA_SECRET=${HCAPTCHA_SECRET}" >> "$ENV_FILE" -[ -n "$HCAPTCHA_SITEKEY" ] && echo "HCAPTCHA_SITEKEY=${HCAPTCHA_SITEKEY}" >> "$ENV_FILE" -[ -n "$PLAUSIBLE_HOST" ] && echo "PLAUSIBLE_HOST=${PLAUSIBLE_HOST}" >> "$ENV_FILE" -[ -n "$PLAUSIBLE_DOMAIN" ] && echo "PLAUSIBLE_DOMAIN=${PLAUSIBLE_DOMAIN}" >> "$ENV_FILE" -[ -n "$SENTRY_DSN" ] && echo "SENTRY_DSN=${SENTRY_DSN}" >> "$ENV_FILE" -[ -n "$SENTRY_FRONT_END_DSN" ] && echo "SENTRY_FRONT_END_DSN=${SENTRY_FRONT_END_DSN}" >> "$ENV_FILE" -[ -n "$FLASK_PROFILER_PATH" ] && echo "FLASK_PROFILER_PATH=${FLASK_PROFILER_PATH}" >> "$ENV_FILE" -[ -n "$FLASK_PROFILER_PASSWORD" ] && echo "FLASK_PROFILER_PASSWORD=${FLASK_PROFILER_PASSWORD}" >> "$ENV_FILE" -[ -n "$HIBP_SCAN_INTERVAL_DAYS" ] && echo "HIBP_SCAN_INTERVAL_DAYS=${HIBP_SCAN_INTERVAL_DAYS}" >> "$ENV_FILE" -[ -n "$HIBP_API_KEYS" ] && echo "HIBP_API_KEYS=${HIBP_API_KEYS}" >> "$ENV_FILE" - -# AWS Storage (If LOCAL_FILE_UPLOAD is overridden) -[ -n "$BUCKET" ] && echo "BUCKET=${BUCKET}" >> "$ENV_FILE" -[ -n "$AWS_ACCESS_KEY_ID" ] && echo "AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}" >> "$ENV_FILE" -[ -n "$AWS_SECRET_ACCESS_KEY" ] && echo "AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}" >> "$ENV_FILE" -[ -n "$AWS_REGION" ] && echo "AWS_REGION=${AWS_REGION}" >> "$ENV_FILE" - -# Paddle Billing (SaaS only) -[ -n "$PADDLE_VENDOR_ID" ] && echo "PADDLE_VENDOR_ID=${PADDLE_VENDOR_ID}" >> "$ENV_FILE" -[ -n "$PADDLE_MONTHLY_PRODUCT_ID" ] && echo "PADDLE_MONTHLY_PRODUCT_ID=${PADDLE_MONTHLY_PRODUCT_ID}" >> "$ENV_FILE" -[ -n "$PADDLE_YEARLY_PRODUCT_ID" ] && echo "PADDLE_YEARLY_PRODUCT_ID=${PADDLE_YEARLY_PRODUCT_ID}" >> "$ENV_FILE" -[ -n "$PADDLE_PUBLIC_KEY_PATH" ] && echo "PADDLE_PUBLIC_KEY_PATH=${PADDLE_PUBLIC_KEY_PATH}" >> "$ENV_FILE" -[ -n "$PADDLE_AUTH_CODE" ] && echo "PADDLE_AUTH_CODE=${PADDLE_AUTH_CODE}" >> "$ENV_FILE" - -# Coinbase Billing (SaaS only) -[ -n "$COINBASE_WEBHOOK_SECRET" ] && echo "COINBASE_WEBHOOK_SECRET=${COINBASE_WEBHOOK_SECRET}" >> "$ENV_FILE" -[ -n "$COINBASE_CHECKOUT_ID" ] && echo "COINBASE_CHECKOUT_ID=${COINBASE_CHECKOUT_ID}" >> "$ENV_FILE" -[ -n "$COINBASE_API_KEY" ] && echo "COINBASE_API_KEY=${COINBASE_API_KEY}" >> "$ENV_FILE" -[ -n "$COINBASE_YEARLY_PRICE" ] && echo "COINBASE_YEARLY_PRICE=${COINBASE_YEARLY_PRICE}" >> "$ENV_FILE" +for optional_var in \ + TEMP_DIR COLOR_LOG LANDING_PAGE_URL STATUS_PAGE_URL ALLOWED_REDIRECT_DOMAINS \ + OTHER_ALIAS_DOMAINS ALIAS_DOMAINS PREMIUM_ALIAS_DOMAINS FIRST_ALIAS_DOMAIN \ + SUPPORT_NAME ADMIN_EMAIL POSTMASTER PGP_SENDER_PRIVATE_KEY_PATH \ + OPENID_PRIVATE_KEY_PATH OPENID_PUBLIC_KEY_PATH ALIAS_LIMIT ALIAS_AUTOMATIC_DISABLE \ + BOUNCE_PREFIX BOUNCE_SUFFIX BOUNCE_PREFIX_FOR_REPLY_PHASE GITHUB_CLIENT_ID \ + GITHUB_CLIENT_SECRET GOOGLE_CLIENT_ID GOOGLE_CLIENT_SECRET FACEBOOK_CLIENT_ID \ + FACEBOOK_CLIENT_SECRET PROTON_CLIENT_ID PROTON_CLIENT_SECRET PROTON_BASE_URL \ + PROTON_VALIDATE_CERTS CONNECT_WITH_PROTON CONNECT_WITH_PROTON_COOKIE_NAME \ + CONNECT_WITH_OIDC_ICON OIDC_WELL_KNOWN_URL OIDC_SCOPES OIDC_NAME_FIELD \ + OIDC_CLIENT_ID OIDC_CLIENT_SECRET APPLE_API_SECRET MACAPP_APPLE_API_SECRET \ + ENABLE_SPAM_ASSASSIN SPAMASSASSIN_HOST HCAPTCHA_SECRET HCAPTCHA_SITEKEY \ + PLAUSIBLE_HOST PLAUSIBLE_DOMAIN SENTRY_DSN SENTRY_FRONT_END_DSN FLASK_PROFILER_PATH \ + FLASK_PROFILER_PASSWORD HIBP_SCAN_INTERVAL_DAYS HIBP_API_KEYS BUCKET AWS_ACCESS_KEY_ID \ + AWS_SECRET_ACCESS_KEY AWS_REGION PADDLE_VENDOR_ID PADDLE_MONTHLY_PRODUCT_ID \ + PADDLE_YEARLY_PRODUCT_ID PADDLE_PUBLIC_KEY_PATH PADDLE_AUTH_CODE COINBASE_WEBHOOK_SECRET \ + COINBASE_CHECKOUT_ID COINBASE_API_KEY COINBASE_YEARLY_PRICE +do + if [ -n "${!optional_var:-}" ]; then + echo "${optional_var}=${!optional_var}" >> "$ENV_FILE" + fi +done -# Secure permissions on .env chmod 600 "$ENV_FILE" echo -e "${GREEN}Successfully generated ${ENV_FILE}${NC}" diff --git a/rootfs/etc/cont-init.d/04-configure-postfix.sh b/rootfs/etc/cont-init.d/04-configure-postfix.sh old mode 100755 new mode 100644 index 6b9af4c..3f75e0b --- a/rootfs/etc/cont-init.d/04-configure-postfix.sh +++ b/rootfs/etc/cont-init.d/04-configure-postfix.sh @@ -1,104 +1,104 @@ #!/command/with-contenv bash -# ============================================================================== -# 04-configure-postfix.sh -# Purpose: Dynamically configures Postfix main.cf and pgsql maps based on the -# SimpleLogin domains and SMTP Relay Modes chosen by the user in Unraid. -# ============================================================================== +set -euo pipefail echo "Configuring Postfix MTA routing..." -# Ensure we have our internal DB credentials +DB_URI_VALUE="${DB_URI:-$(cat /var/run/s6/container_environment/DB_URI 2>/dev/null || true)}" + if [ -f /appdata/postgres/.sl_internal_pass ]; then PG_PASS=$(cat /appdata/postgres/.sl_internal_pass) PG_USER="simplelogin" PG_HOST="127.0.0.1" PG_DB="simplelogin" -elif [ -n "$DB_URI" ]; then - # Parse external DB_URI (postgresql://user:pass@host:port/db) - # Basic extraction for mapped use (fallback) - PG_USER=$(echo "$DB_URI" | sed -n 's/.*:\/\/\([^:]*\).*/\1/p') - PG_PASS=$(echo "$DB_URI" | sed -n 's/.*:\/\/[^:]*:\([^@]*\).*/\1/p') - PG_HOST=$(echo "$DB_URI" | sed -n 's/.*@\([^:]*\).*/\1/p') - PG_DB=$(echo "$DB_URI" | sed -n 's/.*\/\([^?]*\).*/\1/p') +elif [ -n "$DB_URI_VALUE" ]; then + PG_USER=$(echo "$DB_URI_VALUE" | sed -E 's|.*://([^:/]+).*|\1|') + PG_PASS=$(echo "$DB_URI_VALUE" | sed -E 's|.*://[^:]+:([^@]+)@.*|\1|') + PG_HOST=$(echo "$DB_URI_VALUE" | sed -E 's|.*@([^:/]+).*|\1|') + PG_DB=$(echo "$DB_URI_VALUE" | sed -E 's|.*/([^/?]+).*|\1|') +else + echo "Unable to determine PostgreSQL settings for Postfix." + exit 1 fi -# Write pgsql mapping files so Postfix can query aliases instantly -cat << EOM > /etc/postfix/pgsql-virtual-mailbox-domains.cf +cat >/etc/postfix/pgsql-relay-domains.cf < /etc/postfix/pgsql-virtual-mailbox-maps.cf +cat >/etc/postfix/pgsql-transport-maps.cf < /etc/postfix/pgsql-virtual-alias-maps.cf -user = $PG_USER -password = $PG_PASS -hosts = $PG_HOST -dbname = $PG_DB -query = SELECT target_address FROM alias_route WHERE alias_id = (SELECT id FROM alias WHERE email = '%s') AND target_status = 'verified'; -EOM +query = SELECT 'smtp:127.0.0.1:20381' FROM custom_domain WHERE domain = '%s' AND verified = true + UNION SELECT 'smtp:127.0.0.1:20381' WHERE '%s' = '${EMAIL_DOMAIN}' LIMIT 1; +EOF chmod 640 /etc/postfix/pgsql-*.cf chown postfix:postfix /etc/postfix/pgsql-*.cf -# Configure main.cf -postconf -e "myhostname = mail.${EMAIL_DOMAIN}" -postconf -e "mydestination = localhost" -postconf -e "virtual_transport = lmtp:127.0.0.1:20381" -postconf -e "virtual_mailbox_domains = pgsql:/etc/postfix/pgsql-virtual-mailbox-domains.cf" -postconf -e "virtual_mailbox_maps = pgsql:/etc/postfix/pgsql-virtual-mailbox-maps.cf" -postconf -e "virtual_alias_maps = pgsql:/etc/postfix/pgsql-virtual-alias-maps.cf" +if [ ! -f /etc/ssl/private/ssl-cert-snakeoil.key ] || [ ! -f /etc/ssl/certs/ssl-cert-snakeoil.pem ]; then + mkdir -p /etc/ssl/private /etc/ssl/certs + openssl req -x509 -nodes -days 3650 -subj "/CN=mail.${EMAIL_DOMAIN}" \ + -newkey rsa:2048 \ + -keyout /etc/ssl/private/ssl-cert-snakeoil.key \ + -out /etc/ssl/certs/ssl-cert-snakeoil.pem >/dev/null 2>&1 +fi -# ------------------------------------------------------------------------------ -# RELAY MODE CONFIGURATION (Bypassing ISP Port 25 Blocks) -# ------------------------------------------------------------------------------ -echo "Applying SMTP Relay configurations..." +postconf -e "compatibility_level = 2" +postconf -e "myhostname = mail.${EMAIL_DOMAIN}" +postconf -e "mydomain = ${EMAIL_DOMAIN}" +postconf -e "myorigin = ${EMAIL_DOMAIN}" +postconf -e "mydestination =" +postconf -e "inet_interfaces = all" +postconf -e "alias_maps = hash:/etc/aliases" +postconf -e "relay_domains = pgsql:/etc/postfix/pgsql-relay-domains.cf" +postconf -e "transport_maps = pgsql:/etc/postfix/pgsql-transport-maps.cf" +postconf -e "mynetworks = 127.0.0.0/8 [::1]/128" +postconf -e "smtpd_tls_cert_file = /etc/ssl/certs/ssl-cert-snakeoil.pem" +postconf -e "smtpd_tls_key_file = /etc/ssl/private/ssl-cert-snakeoil.key" +postconf -e "smtp_tls_security_level = may" +postconf -e "smtpd_tls_security_level = may" +postconf -e "smtpd_delay_reject = yes" +postconf -e "smtpd_helo_required = yes" +postconf -e "smtpd_helo_restrictions = permit_mynetworks,reject_non_fqdn_helo_hostname,reject_invalid_helo_hostname,permit" +postconf -e "smtpd_sender_restrictions = permit_mynetworks,reject_non_fqdn_sender,reject_unknown_sender_domain,permit" +postconf -e "smtpd_recipient_restrictions = reject_unauth_pipelining,reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_mynetworks,reject_unauth_destination,permit" -# Clear existing sasl_passwd to prevent stale data +echo "Applying SMTP relay configuration..." > /etc/postfix/sasl_passwd -case "$RELAY_MODE" in - "direct") - echo "Using DIRECT delivery (Assuming outbound port 25 is open)." +case "${RELAY_MODE:-direct}" in + direct) postconf -X "relayhost" ;; - "brevo") - echo "Using BREVO relay..." + brevo) postconf -e "relayhost = [smtp-relay.brevo.com]:587" - echo "[smtp-relay.brevo.com]:587 $BREVO_USERNAME:$BREVO_PASSWORD" > /etc/postfix/sasl_passwd + echo "[smtp-relay.brevo.com]:587 ${BREVO_USERNAME}:${BREVO_PASSWORD}" > /etc/postfix/sasl_passwd ;; - "protonmail") - echo "Using PROTONMAIL relay..." - postconf -e "relayhost = [127.0.0.1]:1025" # Assuming user runs Proton bridge - echo "[127.0.0.1]:1025 $SUPPORT_EMAIL:$PROTONMAIL_TOKEN" > /etc/postfix/sasl_passwd + protonmail) + postconf -e "relayhost = [127.0.0.1]:1025" + echo "[127.0.0.1]:1025 ${SUPPORT_EMAIL}:${PROTONMAIL_TOKEN}" > /etc/postfix/sasl_passwd ;; - "gmail") - echo "Using GMAIL relay..." + gmail) postconf -e "relayhost = [smtp.gmail.com]:587" - echo "[smtp.gmail.com]:587 $GMAIL_USERNAME:$GMAIL_APP_PASSWORD" > /etc/postfix/sasl_passwd + echo "[smtp.gmail.com]:587 ${GMAIL_USERNAME}:${GMAIL_APP_PASSWORD}" > /etc/postfix/sasl_passwd ;; - "mailgun") - echo "Using MAILGUN relay..." + mailgun) postconf -e "relayhost = [smtp.mailgun.org]:587" - echo "[smtp.mailgun.org]:587 $MAILGUN_USERNAME:$MAILGUN_PASSWORD" > /etc/postfix/sasl_passwd + echo "[smtp.mailgun.org]:587 ${MAILGUN_USERNAME}:${MAILGUN_PASSWORD}" > /etc/postfix/sasl_passwd ;; - "custom") - echo "Using CUSTOM relay..." - postconf -e "relayhost = $CUSTOM_RELAYHOST" - echo "$CUSTOM_RELAYHOST $CUSTOM_USERNAME:$CUSTOM_PASSWORD" > /etc/postfix/sasl_passwd + custom) + postconf -e "relayhost = ${CUSTOM_RELAYHOST}" + echo "${CUSTOM_RELAYHOST} ${CUSTOM_USERNAME}:${CUSTOM_PASSWORD}" > /etc/postfix/sasl_passwd ;; esac -if [ "$RELAY_MODE" != "direct" ]; then +if [ "${RELAY_MODE:-direct}" != "direct" ]; then postconf -e "smtp_sasl_auth_enable = yes" postconf -e "smtp_sasl_security_options = noanonymous" postconf -e "smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd" @@ -107,4 +107,5 @@ if [ "$RELAY_MODE" != "direct" ]; then chmod 600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db fi +newaliases >/dev/null 2>&1 || true echo "Postfix configuration complete." diff --git a/rootfs/etc/cont-init.d/04-db-migrate.sh b/rootfs/etc/cont-init.d/04-db-migrate.sh deleted file mode 100644 index d4b2254..0000000 --- a/rootfs/etc/cont-init.d/04-db-migrate.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/command/with-contenv bash -# ============================================================================== -# 04-db-migrate.sh -# Purpose: Ensures the database is reachable before attempting to run migrations. -# Runs `flask db upgrade` idempotently on every start. -# Detects first run, executes `init_app.py`, and creates a marker file. -# ============================================================================== - -# ANSI Color Codes -CYAN='\033[1;36m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' - -cd /code || exit 1 - -# Extract DB credentials robustly using a quick python script since the container has psycopg2 -echo -e "${CYAN}Testing database connection...${NC}" - -cat << 'EOF' > /tmp/db_test.py -import os -import sys -from sqlalchemy import create_engine - -db_uri = os.environ.get("DB_URI") -if not db_uri: - sys.exit(1) - -try: - engine = create_engine(db_uri) - conn = engine.connect() - conn.close() - sys.exit(0) -except Exception as e: - sys.exit(1) -EOF - -MAX_RETRIES=30 -RETRY_COUNT=0 -DB_READY=false - -while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do - RETRY_COUNT=$((RETRY_COUNT+1)) - echo -n "Attempt $RETRY_COUNT of $MAX_RETRIES: " - - if python3 /tmp/db_test.py; then - echo -e "${GREEN}Database connection established!${NC}" - DB_READY=true - break - else - echo -e "${YELLOW}Waiting for database to accept connections...${NC}" - sleep 3 - fi -done - -if [ "$DB_READY" = false ]; then - echo -e "${RED}[ERROR] Database is unreachable after $MAX_RETRIES attempts.${NC}" - echo -e "Please check your DB_URI setting and ensure your PostgreSQL container is running." - exit 1 -fi - -# Run Migrations -echo -e "${CYAN}Running database migrations...${NC}" -if ! flask db upgrade; then - echo -e "${RED}[ERROR] Database migration failed.${NC}" - exit 1 -fi -echo -e "${GREEN}Database migrations complete.${NC}" - -# Check for Initialization Marker -MARKER_FILE="/sl/.initialized" - -if [ -f "$MARKER_FILE" ]; then - echo -e "${GREEN}First-run initialization marker found. Skipping init_app.py.${NC}" -else - echo -e "${CYAN}First-run detected. Executing initial database seeding...${NC}" - if ! python3 init_app.py; then - echo -e "${RED}[ERROR] Failed to run init_app.py.${NC}" - exit 1 - fi - - # Create marker file - mkdir -p "$(dirname "$MARKER_FILE")" - touch "$MARKER_FILE" - - echo -e "================================================================================" - echo -e "${GREEN}First-run setup successfully completed!${NC}" - echo -e "================================================================================" - echo -e "Next Steps:" - echo -e "1. Visit your App URL ($URL)" - echo -e "2. Register your administrator account." - echo -e "3. Once registered, edit this container and set ${YELLOW}DISABLE_REGISTRATION=1${NC}" - echo -e "4. Restart the container to lock down your instance from public signups." - echo -e "================================================================================" -fi diff --git a/rootfs/etc/cont-init.d/05-generate-jwt.sh b/rootfs/etc/cont-init.d/05-generate-jwt.sh old mode 100755 new mode 100644 index 8dd6244..4d2c52d --- a/rootfs/etc/cont-init.d/05-generate-jwt.sh +++ b/rootfs/etc/cont-init.d/05-generate-jwt.sh @@ -1,9 +1,5 @@ #!/command/with-contenv bash -# ============================================================================== -# 05-generate-jwt.sh -# Purpose: Auto-generates OpenID Connect RS256 JWT keys if the user enables -# SimpleLogin as an Identity Provider (SSO). -# ============================================================================== +set -euo pipefail CYAN='\033[1;36m' GREEN='\033[0;32m' @@ -13,17 +9,15 @@ JWT_DIR="/appdata/sl" PRIVATE_KEY_FILE="${JWT_DIR}/jwtRS256.key" PUBLIC_KEY_FILE="${JWT_DIR}/jwtRS256.key.pub" -if [ "${ENABLE_OIDC_SERVER}" = "1" ]; then +if [ "${ENABLE_OIDC_SERVER:-0}" = "1" ]; then echo -e "${CYAN}OIDC Server enabled. Checking for JWT Keys...${NC}" - + mkdir -p "$JWT_DIR" chmod 700 "$JWT_DIR" if [ ! -f "$PRIVATE_KEY_FILE" ]; then echo "Generating secure RS256 JWT Keypair for OpenID Connect..." - # Generate private key openssl genrsa -out "$PRIVATE_KEY_FILE" 2048 >/dev/null 2>&1 - # Extract public key openssl rsa -in "$PRIVATE_KEY_FILE" -pubout -out "$PUBLIC_KEY_FILE" >/dev/null 2>&1 chmod 600 "$PRIVATE_KEY_FILE" echo -e "${GREEN}JWT keys generated successfully.${NC}" @@ -31,7 +25,6 @@ if [ "${ENABLE_OIDC_SERVER}" = "1" ]; then echo "Existing JWT keys found. Skipping generation." fi - # Export environmental variable so 03-write-env.sh picks it up - echo "$PRIVATE_KEY_FILE" > /var/run/s6/container_environment/OPENID_PRIVATE_KEY_PATH - echo "$PUBLIC_KEY_FILE" > /var/run/s6/container_environment/OPENID_PUBLIC_KEY_PATH + printf '%s\n' "$PRIVATE_KEY_FILE" > /var/run/s6/container_environment/OPENID_PRIVATE_KEY_PATH + printf '%s\n' "$PUBLIC_KEY_FILE" > /var/run/s6/container_environment/OPENID_PUBLIC_KEY_PATH fi diff --git a/rootfs/etc/services.d/postfix/run b/rootfs/etc/services.d/postfix/run new file mode 100644 index 0000000..f04e73e --- /dev/null +++ b/rootfs/etc/services.d/postfix/run @@ -0,0 +1,9 @@ +#!/command/with-contenv bash +set -euo pipefail + +until nc -z 127.0.0.1 20381 >/dev/null 2>&1; do + echo "Waiting for SimpleLogin email handler on port 20381 before starting Postfix..." + sleep 2 +done + +exec postfix start-fg diff --git a/rootfs/etc/services.d/postgres/run b/rootfs/etc/services.d/postgres/run old mode 100755 new mode 100644 index 0956f52..b52d966 --- a/rootfs/etc/services.d/postgres/run +++ b/rootfs/etc/services.d/postgres/run @@ -1,8 +1,13 @@ #!/command/with-contenv bash -if [ -n "$DB_URI" ] && [[ "$DB_URI" != *"127.0.0.1"* ]]; then - # User provided external DB, don't run internal - sleep infinity +set -euo pipefail + +DB_URI_VALUE="${DB_URI:-$(cat /run/s6/container_environment/DB_URI 2>/dev/null || true)}" + +if [ -n "${DB_URI_VALUE}" ] && [[ "${DB_URI_VALUE}" != *"127.0.0.1"* ]] && [[ "${DB_URI_VALUE}" != *"localhost"* ]]; then + exec sleep infinity fi +mkdir -p /run/postgresql +chown -R postgres:postgres /run/postgresql /appdata/postgres PG_VERSION=$(ls /usr/lib/postgresql/ | head -n 1) -exec s6-setuidgid postgres /usr/lib/postgresql/${PG_VERSION}/bin/postgres -D /appdata/postgres +exec s6-setuidgid postgres /usr/lib/postgresql/${PG_VERSION}/bin/postgres -D /appdata/postgres -c listen_addresses=127.0.0.1 diff --git a/rootfs/etc/services.d/redis/run b/rootfs/etc/services.d/redis/run old mode 100755 new mode 100644 index 15680ae..c438922 --- a/rootfs/etc/services.d/redis/run +++ b/rootfs/etc/services.d/redis/run @@ -1,7 +1,11 @@ #!/command/with-contenv bash -if [ -n "$REDIS_URL" ] && [[ "$REDIS_URL" != *"127.0.0.1"* ]]; then - # User provided external Redis - sleep infinity +set -euo pipefail + +REDIS_URL_VALUE="${REDIS_URL:-$(cat /run/s6/container_environment/REDIS_URL 2>/dev/null || true)}" + +if [ -n "${REDIS_URL_VALUE}" ] && [[ "${REDIS_URL_VALUE}" != *"127.0.0.1"* ]] && [[ "${REDIS_URL_VALUE}" != *"localhost"* ]]; then + exec sleep infinity fi -exec s6-setuidgid redis redis-server --dir /appdata/redis --save 60 1 +chown -R redis:redis /appdata/redis +exec s6-setuidgid redis redis-server --bind 127.0.0.1 --dir /appdata/redis --save 60 1 diff --git a/rootfs/etc/services.d/simplelogin-email/run b/rootfs/etc/services.d/simplelogin-email/run index c9f1ce5..74b3313 100644 --- a/rootfs/etc/services.d/simplelogin-email/run +++ b/rootfs/etc/services.d/simplelogin-email/run @@ -1,3 +1,10 @@ #!/command/with-contenv bash +set -euo pipefail + +until curl -fsS http://127.0.0.1:7777/health >/dev/null; do + echo "Waiting for SimpleLogin web service before starting email handler..." + sleep 2 +done + cd /code || exit 1 -exec s6-setuidgid simplelogin python email_handler.py \ No newline at end of file +exec python email_handler.py diff --git a/rootfs/etc/services.d/simplelogin-job/run b/rootfs/etc/services.d/simplelogin-job/run index 6b17309..6b80ab9 100644 --- a/rootfs/etc/services.d/simplelogin-job/run +++ b/rootfs/etc/services.d/simplelogin-job/run @@ -1,3 +1,10 @@ #!/command/with-contenv bash +set -euo pipefail + +until curl -fsS http://127.0.0.1:7777/health >/dev/null; do + echo "Waiting for SimpleLogin web service before starting job runner..." + sleep 2 +done + cd /code || exit 1 -exec s6-setuidgid simplelogin python job_runner.py \ No newline at end of file +exec python job_runner.py diff --git a/rootfs/etc/services.d/simplelogin-web/run b/rootfs/etc/services.d/simplelogin-web/run index ab49873..18aa89a 100644 --- a/rootfs/etc/services.d/simplelogin-web/run +++ b/rootfs/etc/services.d/simplelogin-web/run @@ -1,5 +1,49 @@ #!/command/with-contenv bash +set -euo pipefail + +wait_for_postgres() { + local db_uri_value="${DB_URI:-$(cat /run/s6/container_environment/DB_URI 2>/dev/null || true)}" + until DB_URI_CHECK="${db_uri_value}" python - <<'PY' +from sqlalchemy import create_engine +from sqlalchemy.exc import SQLAlchemyError +import os + +db_uri = os.environ["DB_URI_CHECK"] +try: + engine = create_engine(db_uri) + with engine.connect(): + pass +except SQLAlchemyError: + raise SystemExit(1) +PY + do + echo "Waiting for PostgreSQL to accept connections..." + sleep 2 + done +} + +wait_for_redis() { + local redis_url_value="${REDIS_URL:-$(cat /run/s6/container_environment/REDIS_URL 2>/dev/null || true)}" + until redis-cli -u "${redis_url_value}" ping >/dev/null 2>&1; do + echo "Waiting for Redis to accept connections..." + sleep 2 + done +} + cd /code || exit 1 -# Default to 2 workers if not specified -WORKERS=${GUNICORN_WORKERS:-2} -exec s6-setuidgid simplelogin gunicorn --bind 0.0.0.0:7777 --workers $WORKERS wsgi:app \ No newline at end of file + +wait_for_postgres +wait_for_redis + +echo "Running SimpleLogin database migrations..." +alembic upgrade head + +MARKER_FILE="/appdata/sl/.initialized" +if [ ! -f "$MARKER_FILE" ]; then + echo "Running SimpleLogin first-run initialization..." + python init_app.py + touch "$MARKER_FILE" +fi + +WORKERS="${GUNICORN_WORKERS:-2}" +exec gunicorn --bind 0.0.0.0:7777 --workers "$WORKERS" wsgi:app diff --git a/scripts/check-upstream.py b/scripts/check-upstream.py new file mode 100755 index 0000000..d8aa5c2 --- /dev/null +++ b/scripts/check-upstream.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import configparser +import json +import os +import pathlib +import re +import sys +import urllib.error +import urllib.request + + +ROOT = pathlib.Path(".") +UPSTREAM_FILE = ROOT / "upstream.toml" +DOCKERFILE = ROOT / "Dockerfile" +SEMVER_RE = re.compile( + r"^v?(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)" + r"(?:-(?P[0-9A-Za-z.-]+))?$" +) + + +def fail(message: str) -> "NoReturn": + print(message, file=sys.stderr) + raise SystemExit(1) + + +def http_json(url: str, headers: dict[str, str] | None = None) -> object: + request = urllib.request.Request( + url, + headers={ + "Accept": "application/vnd.github+json, application/json", + "User-Agent": "jsonbored-simplelogin-aio", + **(headers or {}), + }, + ) + try: + with urllib.request.urlopen(request, timeout=30) as response: + return json.load(response) + except urllib.error.HTTPError as exc: + fail(f"HTTP error while requesting {url}: {exc.code} {exc.reason}") + except urllib.error.URLError as exc: + fail(f"Network error while requesting {url}: {exc.reason}") + + +def parse_version(value: str) -> tuple[int, int, int, bool, str]: + match = SEMVER_RE.match(value) + if not match: + fail(f"Unsupported version format: {value}") + prerelease = match.group("prerelease") + return ( + int(match.group("major")), + int(match.group("minor")), + int(match.group("patch")), + prerelease is not None, + prerelease or "", + ) + + +def version_sort_key(value: str) -> tuple[int, int, int, int, str]: + major, minor, patch, is_prerelease, prerelease = parse_version(value) + return (major, minor, patch, 0 if is_prerelease else 1, prerelease) + + +def github_headers() -> dict[str, str]: + token = os.environ.get("GITHUB_TOKEN", "").strip() + if token: + return {"Authorization": f"Bearer {token}"} + return {} + + +def latest_github_release(repo: str, stable_only: bool) -> str: + data = http_json(f"https://api.github.com/repos/{repo}/releases?per_page=100", github_headers()) + if not isinstance(data, list): + fail(f"Unexpected GitHub releases response for {repo}") + releases: list[str] = [] + for entry in data: + if not isinstance(entry, dict): + continue + tag = entry.get("tag_name") + if not isinstance(tag, str) or not SEMVER_RE.match(tag): + continue + prerelease = bool(entry.get("prerelease")) + if stable_only and prerelease: + continue + releases.append(tag) + if not releases: + fail(f"No matching releases found for upstream repo {repo}") + return sorted(releases, key=version_sort_key)[-1] + + +def read_local_version(config: dict[str, object]) -> str: + version_source = str(config.get("version_source", "")).strip() + version_key = str(config.get("version_key", "")).strip() + if version_source != "dockerfile-arg": + fail(f"Unsupported version_source: {version_source}") + pattern = re.compile(rf"^\s*ARG\s+{re.escape(version_key)}=(.+?)\s*$") + for line in DOCKERFILE.read_text(encoding="utf-8").splitlines(): + match = pattern.match(line) + if match: + return match.group(1) + fail(f"Could not find ARG {version_key} in Dockerfile") + + +def write_local_version(config: dict[str, object], new_version: str) -> None: + version_source = str(config.get("version_source", "")).strip() + version_key = str(config.get("version_key", "")).strip() + if version_source != "dockerfile-arg": + fail(f"Unsupported version_source: {version_source}") + + pattern = re.compile(rf"^(\s*ARG\s+{re.escape(version_key)}=).+?(\s*)$") + updated_lines: list[str] = [] + changed = False + for line in DOCKERFILE.read_text(encoding="utf-8").splitlines(): + match = pattern.match(line) + if match: + updated_lines.append(f"{match.group(1)}{new_version}{match.group(2)}") + changed = True + else: + updated_lines.append(line) + if not changed: + fail(f"Could not update ARG {version_key} in Dockerfile") + DOCKERFILE.write_text("\n".join(updated_lines) + "\n", encoding="utf-8") + + +def write_outputs(outputs: dict[str, str]) -> None: + github_output = os.environ.get("GITHUB_OUTPUT") + if github_output: + with open(github_output, "a", encoding="utf-8") as handle: + for key, value in outputs.items(): + handle.write(f"{key}={value}\n") + else: + for key, value in outputs.items(): + print(f"{key}={value}") + + +def parse_upstream_toml(path: pathlib.Path) -> dict[str, dict[str, object]]: + parser = configparser.ConfigParser() + parser.optionxform = str + parser.read_string(path.read_text(encoding="utf-8")) + + result: dict[str, dict[str, object]] = {} + for section in parser.sections(): + values: dict[str, object] = {} + for key, raw_value in parser.items(section): + value = raw_value.strip() + lower = value.lower() + if lower == "true": + values[key] = True + elif lower == "false": + values[key] = False + else: + values[key] = value.strip('"') + result[section] = values + return result + + +def main() -> None: + if not UPSTREAM_FILE.exists(): + fail("Missing upstream.toml") + + config = parse_upstream_toml(UPSTREAM_FILE) + upstream = config.get("upstream") + notifications = config.get("notifications", {}) + if not isinstance(upstream, dict): + fail("Invalid upstream.toml: missing [upstream]") + + upstream_type = str(upstream.get("type", "")).strip() + stable_only = bool(upstream.get("stable_only", True)) + current_version = read_local_version(upstream) + + if upstream_type == "github-release": + latest_version = latest_github_release(str(upstream.get("repo", "")).strip(), stable_only) + else: + fail(f"Unsupported upstream type: {upstream_type}") + + updates_available = latest_version != current_version + + if os.environ.get("WRITE_UPSTREAM_VERSION") == "true" and updates_available: + write_local_version(upstream, latest_version) + + release_notes = "" + if isinstance(notifications, dict): + release_notes = str(notifications.get("release_notes_url", "")).strip() + if not release_notes and upstream.get("repo"): + release_notes = f"https://github.com/{upstream['repo']}/releases" + + write_outputs( + { + "current_version": current_version, + "latest_version": latest_version, + "updates_available": "true" if updates_available else "false", + "strategy": str(upstream.get("strategy", "pr")).strip() or "pr", + "upstream_name": str(upstream.get("name", "")).strip(), + "release_notes_url": release_notes, + } + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..d3397ab --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_TAG="${1:-simplelogin-aio:test}" +CONTAINER_NAME="simplelogin-aio-smoke" +HOST_HTTP_PORT="${HOST_HTTP_PORT:-57777}" +HOST_SMTP_PORT="${HOST_SMTP_PORT:-52525}" +READY_TIMEOUT_SECONDS="${READY_TIMEOUT_SECONDS:-420}" +KEEP_SMOKE_ARTIFACTS="${KEEP_SMOKE_ARTIFACTS:-0}" + +TMP_APPDATA="$(mktemp -d /tmp/simplelogin-aio-appdata.XXXXXX)" +TMP_PGP="$(mktemp -d /tmp/simplelogin-aio-pgp.XXXXXX)" +cleanup_needed=1 + +cleanup() { + local exit_code=$? + if [[ "$cleanup_needed" -eq 1 ]]; then + docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true + rm -rf "$TMP_APPDATA" "$TMP_PGP" + elif [[ "$exit_code" -ne 0 ]]; then + echo "Smoke test failed; preserving artifacts for debugging." + echo "SMOKE_CONTAINER_NAME=$CONTAINER_NAME" + echo "SMOKE_APPDATA_DIR=$TMP_APPDATA" + echo "SMOKE_PGP_DIR=$TMP_PGP" + fi + exit "$exit_code" +} +trap cleanup EXIT + +start_container() { + docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true + docker run -d \ + --platform linux/amd64 \ + --name "$CONTAINER_NAME" \ + -p "${HOST_HTTP_PORT}:7777" \ + -p "${HOST_SMTP_PORT}:25" \ + -e URL="http://127.0.0.1:${HOST_HTTP_PORT}" \ + -e EMAIL_DOMAIN="example.com" \ + -e SUPPORT_EMAIL="support@example.com" \ + -e FLASK_SECRET="0123456789abcdef0123456789abcdef" \ + -e RELAY_MODE="direct" \ + -e DISABLE_REGISTRATION="1" \ + -e NOT_SEND_EMAIL="true" \ + -e POSTMASTER="postmaster@example.com" \ + -v "${TMP_APPDATA}:/appdata" \ + -v "${TMP_PGP}:/pgp" \ + "$IMAGE_TAG" >/dev/null +} + +wait_for_http() { + local deadline=$((SECONDS + READY_TIMEOUT_SECONDS)) + while (( SECONDS < deadline )); do + if curl -fsS "http://127.0.0.1:${HOST_HTTP_PORT}/health" >/dev/null; then + return 0 + fi + docker ps --format '{{.Names}}' | grep -qx "$CONTAINER_NAME" + sleep 2 + done + echo "SimpleLogin web service did not become healthy in time." + docker logs "$CONTAINER_NAME" || true + return 1 +} + +wait_for_smtp() { + local deadline=$((SECONDS + 120)) + while (( SECONDS < deadline )); do + if nc -z 127.0.0.1 "$HOST_SMTP_PORT" >/dev/null 2>&1; then + return 0 + fi + sleep 2 + done + echo "SimpleLogin SMTP service did not become ready in time." + docker logs "$CONTAINER_NAME" || true + return 1 +} + +verify_persistence() { + docker exec "$CONTAINER_NAME" test -f /appdata/postgres/PG_VERSION + docker exec "$CONTAINER_NAME" test -f /appdata/sl/.initialized +} + +start_container +wait_for_http +wait_for_smtp +verify_persistence + +docker restart "$CONTAINER_NAME" >/dev/null +wait_for_http +wait_for_smtp +verify_persistence + +if [[ "$KEEP_SMOKE_ARTIFACTS" -eq 1 ]]; then + cleanup_needed=0 +fi diff --git a/upstream.toml b/upstream.toml new file mode 100644 index 0000000..42c960e --- /dev/null +++ b/upstream.toml @@ -0,0 +1,11 @@ +[upstream] +name = "SimpleLogin" +type = "github-release" +repo = "simple-login/app" +version_source = "dockerfile-arg" +version_key = "UPSTREAM_VERSION" +strategy = "pr" +stable_only = true + +[notifications] +release_notes_url = "https://github.com/simple-login/app/releases" From 54721a357a87f5945e7223ad31dc6a16dcff9981 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:40:19 -0600 Subject: [PATCH 2/2] Fix smoke test bind mount permissions --- rootfs/etc/cont-init.d/01-bootstrap-databases.sh | 3 +++ scripts/smoke-test.sh | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/rootfs/etc/cont-init.d/01-bootstrap-databases.sh b/rootfs/etc/cont-init.d/01-bootstrap-databases.sh index 7f76cd0..f70439a 100644 --- a/rootfs/etc/cont-init.d/01-bootstrap-databases.sh +++ b/rootfs/etc/cont-init.d/01-bootstrap-databases.sh @@ -3,10 +3,13 @@ set -euo pipefail echo "Checking internal database requirements..." +chmod 755 /appdata mkdir -p /appdata/postgres /appdata/redis /appdata/dkim /appdata/sl/upload /pgp /run/postgresql chown -R redis:redis /appdata/redis chown -R postgres:postgres /appdata/postgres /run/postgresql chown -R simplelogin:simplelogin /appdata/sl /pgp +chmod 755 /appdata/sl +chmod 700 /appdata/postgres /appdata/redis /pgp chmod 700 /pgp rm -rf /code/static/upload diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index d3397ab..3e7fd18 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -10,12 +10,19 @@ KEEP_SMOKE_ARTIFACTS="${KEEP_SMOKE_ARTIFACTS:-0}" TMP_APPDATA="$(mktemp -d /tmp/simplelogin-aio-appdata.XXXXXX)" TMP_PGP="$(mktemp -d /tmp/simplelogin-aio-pgp.XXXXXX)" +chmod 755 "$TMP_APPDATA" cleanup_needed=1 cleanup() { local exit_code=$? if [[ "$cleanup_needed" -eq 1 ]]; then docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true + if [[ -d "$TMP_APPDATA" ]]; then + docker run --rm -v "${TMP_APPDATA}:/cleanup" --entrypoint sh "$IMAGE_TAG" -c 'rm -rf /cleanup/* /cleanup/.[!.]* /cleanup/..?* 2>/dev/null || true' >/dev/null 2>&1 || true + fi + if [[ -d "$TMP_PGP" ]]; then + docker run --rm -v "${TMP_PGP}:/cleanup" --entrypoint sh "$IMAGE_TAG" -c 'rm -rf /cleanup/* /cleanup/.[!.]* /cleanup/..?* 2>/dev/null || true' >/dev/null 2>&1 || true + fi rm -rf "$TMP_APPDATA" "$TMP_PGP" elif [[ "$exit_code" -ne 0 ]]; then echo "Smoke test failed; preserving artifacts for debugging."