From d3f0b36dd2b942c95f8e8eec0a9ad6af404a0fee Mon Sep 17 00:00:00 2001 From: John Coffey Date: Fri, 18 Sep 2026 11:09:22 -0700 Subject: [PATCH] Packaging: the binary and package are inbuxa, INBUXA_* settings with STALWART_* fallback - crates/main: package and [[bin]] renamed to inbuxa; homepage inbuxa.org; license AGPL-3.0-only (upstream is dual; the fork takes the AGPL). - types::branding::env_var reads INBUXA_, falling back to STALWART_ with a warning, for all nine server settings. STALWART_APP_ and STALWART_SPAM_* storage keys are unchanged. - New-install default paths /var/lib/inbuxa and /var/log/inbuxa. - Dockerfiles, systemd unit, launchd plist and AppArmor profile renamed. - Upstream's .github moved to .github-upstream so none of it runs. - install.sh stubbed: upstream's would install Stalwart. - Two missed brand strings: the SMTP Received header and the utils user agent. --- .../community-discussions.yml | 0 .../ISSUE_TEMPLATE/config.yml | 0 .../ISSUE_TEMPLATE/confirmed_issue.yml | 0 .../allowed-pr-authors.txt | 0 {.github => .github-upstream}/dependabot.yml | 0 .../workflows/auto-close-issues.yml | 0 .../workflows/auto-close-prs.yml | 0 .../workflows/auto-redirect-discussions.yml | 0 .../workflows/ci-retry.yml | 0 .../workflows/ci.yml | 0 .../workflows/scorecard.yml | 0 .../workflows/test.yml | 0 .../workflows/trivy.yml | 0 Cargo.lock | 64 +- Dockerfile | 28 +- Dockerfile.build | 64 +- Dockerfile.fdb | 28 +- README.md | 10 +- crates/common/src/config/server/listener.rs | 2 +- crates/common/src/config/telemetry.rs | 2 +- crates/common/src/manager/boot.rs | 2 +- crates/common/src/manager/defaults.rs | 2 +- crates/jmap/src/registry/mapping/bootstrap.rs | 4 +- crates/main/Cargo.toml | 10 +- crates/registry/src/schema/structs_impl.rs | 4 +- crates/smtp/src/inbound/data.rs | 2 +- crates/store/src/build/registry.rs | 6 +- crates/store/src/registry/local.rs | 14 +- crates/types/src/branding.rs | 19 + crates/utils/src/http.rs | 2 +- docs/spec/SPEC.md | 36 +- install.sh | 1077 +---------------- .../apparmor.d/{stalwart-mail => inbuxa} | 12 +- ...{stalwart.mail.plist => inbuxa.mail.plist} | 6 +- .../{stalwart-mail.service => inbuxa.service} | 10 +- 35 files changed, 199 insertions(+), 1205 deletions(-) rename {.github => .github-upstream}/DISCUSSION_TEMPLATE/community-discussions.yml (100%) rename {.github => .github-upstream}/ISSUE_TEMPLATE/config.yml (100%) rename {.github => .github-upstream}/ISSUE_TEMPLATE/confirmed_issue.yml (100%) rename {.github => .github-upstream}/allowed-pr-authors.txt (100%) rename {.github => .github-upstream}/dependabot.yml (100%) rename {.github => .github-upstream}/workflows/auto-close-issues.yml (100%) rename {.github => .github-upstream}/workflows/auto-close-prs.yml (100%) rename {.github => .github-upstream}/workflows/auto-redirect-discussions.yml (100%) rename {.github => .github-upstream}/workflows/ci-retry.yml (100%) rename {.github => .github-upstream}/workflows/ci.yml (100%) rename {.github => .github-upstream}/workflows/scorecard.yml (100%) rename {.github => .github-upstream}/workflows/test.yml (100%) rename {.github => .github-upstream}/workflows/trivy.yml (100%) rename resources/apparmor.d/{stalwart-mail => inbuxa} (88%) rename resources/systemd/{stalwart.mail.plist => inbuxa.mail.plist} (79%) rename resources/systemd/{stalwart-mail.service => inbuxa.service} (68%) diff --git a/.github/DISCUSSION_TEMPLATE/community-discussions.yml b/.github-upstream/DISCUSSION_TEMPLATE/community-discussions.yml similarity index 100% rename from .github/DISCUSSION_TEMPLATE/community-discussions.yml rename to .github-upstream/DISCUSSION_TEMPLATE/community-discussions.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github-upstream/ISSUE_TEMPLATE/config.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/config.yml rename to .github-upstream/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/confirmed_issue.yml b/.github-upstream/ISSUE_TEMPLATE/confirmed_issue.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/confirmed_issue.yml rename to .github-upstream/ISSUE_TEMPLATE/confirmed_issue.yml diff --git a/.github/allowed-pr-authors.txt b/.github-upstream/allowed-pr-authors.txt similarity index 100% rename from .github/allowed-pr-authors.txt rename to .github-upstream/allowed-pr-authors.txt diff --git a/.github/dependabot.yml b/.github-upstream/dependabot.yml similarity index 100% rename from .github/dependabot.yml rename to .github-upstream/dependabot.yml diff --git a/.github/workflows/auto-close-issues.yml b/.github-upstream/workflows/auto-close-issues.yml similarity index 100% rename from .github/workflows/auto-close-issues.yml rename to .github-upstream/workflows/auto-close-issues.yml diff --git a/.github/workflows/auto-close-prs.yml b/.github-upstream/workflows/auto-close-prs.yml similarity index 100% rename from .github/workflows/auto-close-prs.yml rename to .github-upstream/workflows/auto-close-prs.yml diff --git a/.github/workflows/auto-redirect-discussions.yml b/.github-upstream/workflows/auto-redirect-discussions.yml similarity index 100% rename from .github/workflows/auto-redirect-discussions.yml rename to .github-upstream/workflows/auto-redirect-discussions.yml diff --git a/.github/workflows/ci-retry.yml b/.github-upstream/workflows/ci-retry.yml similarity index 100% rename from .github/workflows/ci-retry.yml rename to .github-upstream/workflows/ci-retry.yml diff --git a/.github/workflows/ci.yml b/.github-upstream/workflows/ci.yml similarity index 100% rename from .github/workflows/ci.yml rename to .github-upstream/workflows/ci.yml diff --git a/.github/workflows/scorecard.yml b/.github-upstream/workflows/scorecard.yml similarity index 100% rename from .github/workflows/scorecard.yml rename to .github-upstream/workflows/scorecard.yml diff --git a/.github/workflows/test.yml b/.github-upstream/workflows/test.yml similarity index 100% rename from .github/workflows/test.yml rename to .github-upstream/workflows/test.yml diff --git a/.github/workflows/trivy.yml b/.github-upstream/workflows/trivy.yml similarity index 100% rename from .github/workflows/trivy.yml rename to .github-upstream/workflows/trivy.yml diff --git a/Cargo.lock b/Cargo.lock index 55cb635..934972d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3923,6 +3923,38 @@ dependencies = [ "utils", ] +[[package]] +name = "inbuxa" +version = "0.16.22" +dependencies = [ + "common", + "coordinator", + "dav", + "directory", + "email", + "groupware", + "http 0.16.22", + "http_proto", + "imap", + "jmap", + "managesieve", + "migration", + "pop3", + "registry", + "rustls", + "scim", + "services", + "smtp", + "smtp-proto", + "spam-filter", + "store", + "tikv-jemallocator", + "tokio", + "trc", + "types", + "utils", +] + [[package]] name = "include-flate" version = "0.3.4" @@ -8222,38 +8254,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "stalwart" -version = "0.16.22" -dependencies = [ - "common", - "coordinator", - "dav", - "directory", - "email", - "groupware", - "http 0.16.22", - "http_proto", - "imap", - "jmap", - "managesieve", - "migration", - "pop3", - "registry", - "rustls", - "scim", - "services", - "smtp", - "smtp-proto", - "spam-filter", - "store", - "tikv-jemallocator", - "tokio", - "trc", - "types", - "utils", -] - [[package]] name = "static_assertions" version = "1.1.0" diff --git a/Dockerfile b/Dockerfile index 11ead51..7fb60d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ RUN rustup target add "$(cat /target.txt)" COPY --from=planner /recipe.json /recipe.json RUN RUSTFLAGS="$(cat /flags.txt)" cargo chef cook --target "$(cat /target.txt)" --release --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" --recipe-path /recipe.json COPY . . -RUN RUSTFLAGS="$(cat /flags.txt)" cargo build --target "$(cat /target.txt)" --release -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" +RUN RUSTFLAGS="$(cat /flags.txt)" cargo build --target "$(cat /target.txt)" --release -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" RUN mv "/build/target/$(cat /target.txt)/release" "/output" FROM docker.io/debian:trixie-slim @@ -29,18 +29,18 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ apt-get update && \ apt-get install -yq --no-install-recommends ca-certificates curl libcap2-bin && \ rm -rf /var/lib/apt/lists/* && \ - groupadd -r -g 2000 stalwart && \ - useradd -r -u 2000 -g 2000 -s /usr/sbin/nologin -M stalwart && \ - mkdir -p /etc/stalwart /var/lib/stalwart && \ - chown stalwart:stalwart /etc/stalwart /var/lib/stalwart -COPY --from=builder --chmod=0755 /output/stalwart /usr/local/bin/stalwart -RUN setcap 'cap_net_bind_service=+ep' /usr/local/bin/stalwart -USER stalwart -WORKDIR /var/lib/stalwart -VOLUME ["/etc/stalwart", "/var/lib/stalwart"] + groupadd -r -g 2000 inbuxa && \ + useradd -r -u 2000 -g 2000 -s /usr/sbin/nologin -M inbuxa && \ + mkdir -p /etc/inbuxa /var/lib/inbuxa && \ + chown inbuxa:inbuxa /etc/inbuxa /var/lib/inbuxa +COPY --from=builder --chmod=0755 /output/inbuxa /usr/local/bin/inbuxa +RUN setcap 'cap_net_bind_service=+ep' /usr/local/bin/inbuxa +USER inbuxa +WORKDIR /var/lib/inbuxa +VOLUME ["/etc/inbuxa", "/var/lib/inbuxa"] EXPOSE 443 25 110 587 465 143 993 995 4190 8080 -ENV STALWART_HEALTHCHECK_URL=https://127.0.0.1:443/healthz/live +ENV INBUXA_HEALTHCHECK_URL=https://127.0.0.1:443/healthz/live HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ - CMD curl -fsSk -H "X-Forwarded-For: 127.0.0.1" "$STALWART_HEALTHCHECK_URL" || curl -fsS -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/healthz/live || exit 1 -ENTRYPOINT ["/usr/local/bin/stalwart"] -CMD ["--config", "/etc/stalwart/config.json"] + CMD curl -fsSk -H "X-Forwarded-For: 127.0.0.1" "$INBUXA_HEALTHCHECK_URL" || curl -fsS -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/healthz/live || exit 1 +ENTRYPOINT ["/usr/local/bin/inbuxa"] +CMD ["--config", "/etc/inbuxa/config.json"] diff --git a/Dockerfile.build b/Dockerfile.build index db6686d..f1401c9 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -108,7 +108,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ if [ ! -z "${FDB_ARCH}" ]; then \ - RUSTFLAGS="-L /usr/lib" cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats"; \ + RUSTFLAGS="-L /usr/lib" cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p inbuxa --no-default-features --features "foundationdb s3 redis nats"; \ fi RUN \ --mount=type=secret,id=ACTIONS_RESULTS_URL,env=ACTIONS_RESULTS_URL \ @@ -116,7 +116,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ - cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" + cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" # Copy the source code COPY . . ENV RUSTC_WRAPPER="sccache" \ @@ -129,8 +129,8 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ if [ ! -z "${FDB_ARCH}" ]; then \ - RUSTFLAGS="-L /usr/lib" cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats" && \ - mv /app/target/${TARGET}/release/stalwart /app/artifact/stalwart-foundationdb; \ + RUSTFLAGS="-L /usr/lib" cargo zigbuild --release --target ${TARGET} -p inbuxa --no-default-features --features "foundationdb s3 redis nats" && \ + mv /app/target/${TARGET}/release/inbuxa /app/artifact/inbuxa-foundationdb; \ fi # Build generic version RUN \ @@ -139,8 +139,8 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ - cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" && \ - mv /app/target/${TARGET}/release/stalwart /app/artifact/stalwart + cargo zigbuild --release --target ${TARGET} -p inbuxa --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" && \ + mv /app/target/${TARGET}/release/inbuxa /app/artifact/inbuxa # ***************** # Binary stage @@ -156,21 +156,21 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ apt-get update && \ apt-get install -yq --no-install-recommends ca-certificates curl tzdata libcap2-bin && \ rm -rf /var/lib/apt/lists/* && \ - groupadd -r -g 2000 stalwart && \ - useradd -r -u 2000 -g 2000 -s /usr/sbin/nologin -M stalwart && \ - mkdir -p /etc/stalwart /var/lib/stalwart && \ - chown stalwart:stalwart /etc/stalwart /var/lib/stalwart -COPY --from=builder --chmod=0755 /app/artifact/stalwart /usr/local/bin/stalwart -RUN setcap 'cap_net_bind_service=+ep' /usr/local/bin/stalwart -USER stalwart -WORKDIR /var/lib/stalwart -VOLUME ["/etc/stalwart", "/var/lib/stalwart"] + groupadd -r -g 2000 inbuxa && \ + useradd -r -u 2000 -g 2000 -s /usr/sbin/nologin -M inbuxa && \ + mkdir -p /etc/inbuxa /var/lib/inbuxa && \ + chown inbuxa:inbuxa /etc/inbuxa /var/lib/inbuxa +COPY --from=builder --chmod=0755 /app/artifact/inbuxa /usr/local/bin/inbuxa +RUN setcap 'cap_net_bind_service=+ep' /usr/local/bin/inbuxa +USER inbuxa +WORKDIR /var/lib/inbuxa +VOLUME ["/etc/inbuxa", "/var/lib/inbuxa"] EXPOSE 443 25 110 587 465 143 993 995 4190 8080 -ENV STALWART_HEALTHCHECK_URL=https://127.0.0.1:443/healthz/live +ENV INBUXA_HEALTHCHECK_URL=https://127.0.0.1:443/healthz/live HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ - CMD curl -fsSk -H "X-Forwarded-For: 127.0.0.1" "$STALWART_HEALTHCHECK_URL" || curl -fsS -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/healthz/live || exit 1 -ENTRYPOINT ["/usr/local/bin/stalwart"] -CMD ["--config", "/etc/stalwart/config.json"] + CMD curl -fsSk -H "X-Forwarded-For: 127.0.0.1" "$INBUXA_HEALTHCHECK_URL" || curl -fsS -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/healthz/live || exit 1 +ENTRYPOINT ["/usr/local/bin/inbuxa"] +CMD ["--config", "/etc/inbuxa/config.json"] # ***************** # Runtime image for musl targets @@ -178,18 +178,18 @@ CMD ["--config", "/etc/stalwart/config.json"] FROM --platform=$TARGETPLATFORM alpine AS musl RUN apk add --update --no-cache ca-certificates curl tzdata libcap && \ rm -rf /var/cache/apk/* && \ - addgroup -S -g 2000 stalwart && \ - adduser -S -D -H -u 2000 -G stalwart -s /sbin/nologin stalwart && \ - mkdir -p /etc/stalwart /var/lib/stalwart && \ - chown stalwart:stalwart /etc/stalwart /var/lib/stalwart -COPY --from=builder --chmod=0755 /app/artifact/stalwart /usr/local/bin/stalwart -RUN setcap 'cap_net_bind_service=+ep' /usr/local/bin/stalwart -USER stalwart -WORKDIR /var/lib/stalwart -VOLUME ["/etc/stalwart", "/var/lib/stalwart"] + addgroup -S -g 2000 inbuxa && \ + adduser -S -D -H -u 2000 -G inbuxa -s /sbin/nologin inbuxa && \ + mkdir -p /etc/inbuxa /var/lib/inbuxa && \ + chown inbuxa:inbuxa /etc/inbuxa /var/lib/inbuxa +COPY --from=builder --chmod=0755 /app/artifact/inbuxa /usr/local/bin/inbuxa +RUN setcap 'cap_net_bind_service=+ep' /usr/local/bin/inbuxa +USER inbuxa +WORKDIR /var/lib/inbuxa +VOLUME ["/etc/inbuxa", "/var/lib/inbuxa"] EXPOSE 443 25 110 587 465 143 993 995 4190 8080 -ENV STALWART_HEALTHCHECK_URL=https://127.0.0.1:443/healthz/live +ENV INBUXA_HEALTHCHECK_URL=https://127.0.0.1:443/healthz/live HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ - CMD curl -fsSk -H "X-Forwarded-For: 127.0.0.1" "$STALWART_HEALTHCHECK_URL" || curl -fsS -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/healthz/live || exit 1 -ENTRYPOINT ["/usr/local/bin/stalwart"] -CMD ["--config", "/etc/stalwart/config.json"] + CMD curl -fsSk -H "X-Forwarded-For: 127.0.0.1" "$INBUXA_HEALTHCHECK_URL" || curl -fsS -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/healthz/live || exit 1 +ENTRYPOINT ["/usr/local/bin/inbuxa"] +CMD ["--config", "/etc/inbuxa/config.json"] diff --git a/Dockerfile.fdb b/Dockerfile.fdb index a78fc93..f7c309e 100644 --- a/Dockerfile.fdb +++ b/Dockerfile.fdb @@ -53,28 +53,28 @@ COPY Cargo.lock . COPY crates/ crates/ COPY resources/ resources/ COPY tests/ tests/ -RUN cargo build -p stalwart --no-default-features --features "foundationdb s3 redis azure nats" --release +RUN cargo build -p inbuxa --no-default-features --features "foundationdb s3 redis azure nats" --release FROM debian:trixie-slim AS runtime -COPY --from=builder --chmod=0755 /app/target/release/stalwart /usr/local/bin/stalwart +COPY --from=builder --chmod=0755 /app/target/release/inbuxa /usr/local/bin/inbuxa COPY --from=builder /usr/lib/libfdb_c.so /usr/lib/libfdb_c.so RUN export DEBIAN_FRONTEND=noninteractive && \ apt-get update && \ apt-get install -yq --no-install-recommends ca-certificates curl libcap2-bin && \ rm -rf /var/lib/apt/lists/* && \ - groupadd -r -g 2000 stalwart && \ - useradd -r -u 2000 -g 2000 -s /usr/sbin/nologin -M stalwart && \ - mkdir -p /etc/stalwart /var/lib/stalwart && \ - chown stalwart:stalwart /etc/stalwart /var/lib/stalwart && \ - setcap 'cap_net_bind_service=+ep' /usr/local/bin/stalwart + groupadd -r -g 2000 inbuxa && \ + useradd -r -u 2000 -g 2000 -s /usr/sbin/nologin -M inbuxa && \ + mkdir -p /etc/inbuxa /var/lib/inbuxa && \ + chown inbuxa:inbuxa /etc/inbuxa /var/lib/inbuxa && \ + setcap 'cap_net_bind_service=+ep' /usr/local/bin/inbuxa -USER stalwart -WORKDIR /var/lib/stalwart -VOLUME ["/etc/stalwart", "/var/lib/stalwart"] +USER inbuxa +WORKDIR /var/lib/inbuxa +VOLUME ["/etc/inbuxa", "/var/lib/inbuxa"] EXPOSE 443 25 110 587 465 143 993 995 4190 8080 -ENV STALWART_HEALTHCHECK_URL=https://127.0.0.1:443/healthz/live +ENV INBUXA_HEALTHCHECK_URL=https://127.0.0.1:443/healthz/live HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ - CMD curl -fsSk -H "X-Forwarded-For: 127.0.0.1" "$STALWART_HEALTHCHECK_URL" || curl -fsS -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/healthz/live || exit 1 -ENTRYPOINT ["/usr/local/bin/stalwart"] -CMD ["--config", "/etc/stalwart/config.json"] + CMD curl -fsSk -H "X-Forwarded-For: 127.0.0.1" "$INBUXA_HEALTHCHECK_URL" || curl -fsS -H "X-Forwarded-For: 127.0.0.1" http://127.0.0.1:8080/healthz/live || exit 1 +ENTRYPOINT ["/usr/local/bin/inbuxa"] +CMD ["--config", "/etc/inbuxa/config.json"] diff --git a/README.md b/README.md index 9099702..0e74627 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,15 @@ The report for every import is in `docs/fork/strip-reports/`. See ## Building ```bash -cargo build --release -p stalwart +cargo build --release -p inbuxa # the binary is target/release/inbuxa +docker build -t inbuxa . # or the container image ``` -The binary and package are still named `stalwart` while the packaging is -reworked. +Settings are read from `INBUXA_*` environment variables. An existing Stalwart +install's `STALWART_*` variables still work, with a warning to rename them. +New installs keep their data in `/var/lib/inbuxa` and logs in +`/var/log/inbuxa`. Existing installs keep the paths their configuration +already names, so none of their data moves. ## License and credits diff --git a/crates/common/src/config/server/listener.rs b/crates/common/src/config/server/listener.rs index 869015d..38b5a9e 100644 --- a/crates/common/src/config/server/listener.rs +++ b/crates/common/src/config/server/listener.rs @@ -69,7 +69,7 @@ impl Listeners { bind: Map::new(vec![ SocketAddr::from_str(&format!( "[::]:{}", - std::env::var("STALWART_RECOVERY_MODE_PORT") + types::branding::env_var("RECOVERY_MODE_PORT") .ok() .and_then(|p| p.parse::().ok()) .unwrap_or(8080) diff --git a/crates/common/src/config/telemetry.rs b/crates/common/src/config/telemetry.rs index fea1760..914e06a 100644 --- a/crates/common/src/config/telemetry.rs +++ b/crates/common/src/config/telemetry.rs @@ -503,7 +503,7 @@ impl Tracers { } } else { // Add default tracer if none were found - let level = std::env::var("STALWART_RECOVERY_MODE_LOG_LEVEL") + let level = types::branding::env_var("RECOVERY_MODE_LOG_LEVEL") .ok() .and_then(|level| Level::from_str(&level).ok()) .unwrap_or(Level::Info); diff --git a/crates/common/src/manager/boot.rs b/crates/common/src/manager/boot.rs index bc00361..1d99026 100644 --- a/crates/common/src/manager/boot.rs +++ b/crates/common/src/manager/boot.rs @@ -43,7 +43,7 @@ const HELP: &str = concat!( env!("CARGO_PKG_VERSION"), r#" -Usage: stalwart [OPTIONS] +Usage: inbuxa [OPTIONS] Options: -c, --config Start server with the specified configuration file diff --git a/crates/common/src/manager/defaults.rs b/crates/common/src/manager/defaults.rs index 698af40..e6e74da 100644 --- a/crates/common/src/manager/defaults.rs +++ b/crates/common/src/manager/defaults.rs @@ -529,7 +529,7 @@ async fn insert_safe_defaults(bp: &mut Bootstrap) -> trc::Result<()> { ansi: false, prefix: "stalwart.log".into(), rotate: LogRotateFrequency::Daily, - path: "/var/log/stalwart".into(), + path: "/var/log/inbuxa".into(), ..Default::default() }) .into(), diff --git a/crates/jmap/src/registry/mapping/bootstrap.rs b/crates/jmap/src/registry/mapping/bootstrap.rs index 900abdb..b253025 100644 --- a/crates/jmap/src/registry/mapping/bootstrap.rs +++ b/crates/jmap/src/registry/mapping/bootstrap.rs @@ -657,7 +657,7 @@ fn map_dns_server(dns_server: &DnsServerBootstrap) -> Option Bootstrap { @@ -676,7 +676,7 @@ fn build_default_bootstrap(server: &Server) -> Bootstrap { in_memory_store: InMemoryStore::Default, directory: DirectoryBootstrap::Internal, tracer: Tracer::Log(TracerLog { - path: "/var/log/stalwart/".to_string(), + path: "/var/log/inbuxa/".to_string(), prefix: "stalwart".to_string(), ansi: true, enable: true, diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index 394cd2f..ed5f73c 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -1,17 +1,17 @@ [package] -name = "stalwart" +name = "inbuxa" description = "INBUXA Mail and Collaboration Server, a fork of Stalwart" authors = [ "Stalwart Labs LLC "] -repository = "https://github.com/stalwartlabs/stalwart" -homepage = "https://stalw.art" +homepage = "https://inbuxa.org" keywords = ["imap", "jmap", "smtp", "email", "mail", "webdav", "server"] categories = ["email"] -license = "AGPL-3.0-only OR LicenseRef-SEL" +# Upstream offers AGPL-3.0-only OR LicenseRef-SEL; INBUXA takes the AGPL only. +license = "AGPL-3.0-only" version = "0.16.22" edition = "2024" [[bin]] -name = "stalwart" +name = "inbuxa" path = "src/main.rs" [dependencies] diff --git a/crates/registry/src/schema/structs_impl.rs b/crates/registry/src/schema/structs_impl.rs index 1d65d22..7708e0c 100644 --- a/crates/registry/src/schema/structs_impl.rs +++ b/crates/registry/src/schema/structs_impl.rs @@ -4143,7 +4143,7 @@ impl Default for Bootstrap { request_tls_certificate: true, generate_dkim_keys: true, data_store: DataStore::RocksDb(RocksDbStore { - path: "/var/lib/stalwart/".to_string(), + path: "/var/lib/inbuxa/".to_string(), ..Default::default() }), blob_store: BlobStore::Default, @@ -4151,7 +4151,7 @@ impl Default for Bootstrap { in_memory_store: InMemoryStore::Default, directory: DirectoryBootstrap::Internal, tracer: Tracer::Log(TracerLog { - path: "/var/log/stalwart/".to_string(), + path: "/var/log/inbuxa/".to_string(), ..Default::default() }), dns_server: DnsServerBootstrap::Manual, diff --git a/crates/smtp/src/inbound/data.rs b/crates/smtp/src/inbound/data.rs index d9198c3..37b0eb8 100644 --- a/crates/smtp/src/inbound/data.rs +++ b/crates/smtp/src/inbound/data.rs @@ -1051,7 +1051,7 @@ impl Session { } headers.extend_from_slice(b"by "); headers.extend_from_slice(self.hostname.as_bytes()); - headers.extend_from_slice(b" (Stalwart SMTP) with "); + headers.extend_from_slice(concat!(" (", types::brand!(), " SMTP) with ").as_bytes()); headers.extend_from_slice(match (self.stream.is_tls(), !self.is_authenticated()) { (true, true) => b"ESMTPS", (true, false) => b"ESMTPSA", diff --git a/crates/store/src/build/registry.rs b/crates/store/src/build/registry.rs index ac622a4..b244d81 100644 --- a/crates/store/src/build/registry.rs +++ b/crates/store/src/build/registry.rs @@ -31,18 +31,18 @@ impl RegistryStore { .collect::(); eprintln!(); eprintln!("════════════════════════════════════════════════════════════"); - eprintln!("🔑 Stalwart bootstrap mode - temporary administrator account"); + eprintln!("🔑 INBUXA bootstrap mode - temporary administrator account"); eprintln!(); eprintln!(" username: admin"); eprintln!(" password: {password}"); eprintln!(); eprintln!("Use these credentials to complete the initial setup at the"); - eprintln!("/admin web UI. Once setup is done, Stalwart will provision a"); + eprintln!("/admin web UI. Once setup is done, the server will provision a"); eprintln!("permanent administrator and this temporary account will no"); eprintln!("longer apply."); eprintln!(); eprintln!("This password is shown only once. To pin a credential"); - eprintln!("instead, set STALWART_RECOVERY_ADMIN=admin: in the"); + eprintln!("instead, set INBUXA_RECOVERY_ADMIN=admin: in the"); eprintln!("env file."); eprintln!("════════════════════════════════════════════════════════════"); eprintln!(); diff --git a/crates/store/src/registry/local.rs b/crates/store/src/registry/local.rs index 85d091a..5dbfd38 100644 --- a/crates/store/src/registry/local.rs +++ b/crates/store/src/registry/local.rs @@ -17,7 +17,7 @@ pub(crate) enum RegistryInit { impl RegistryStoreInner { pub(crate) fn new(local_path: PathBuf) -> Self { - let env_hostname = std::env::var("STALWART_HOSTNAME") + let env_hostname = types::branding::env_var("HOSTNAME") .ok() .filter(|h| !h.is_empty()) .unwrap_or_else(|| { @@ -35,30 +35,30 @@ impl RegistryStoreInner { store: Store::None, id_generator: SnowflakeIdGenerator::new(), node_id: 0, - env_recovery_mode: std::env::var("STALWART_RECOVERY_MODE") + env_recovery_mode: types::branding::env_var("RECOVERY_MODE") .ok() .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) .unwrap_or(false), - env_recovery_admin: std::env::var("STALWART_RECOVERY_ADMIN") + env_recovery_admin: types::branding::env_var("RECOVERY_ADMIN") .ok() .and_then(|v| { v.split_once(':') .map(|(a, p)| (a.trim().to_string(), p.trim().to_string())) }) .filter(|(a, p)| !a.is_empty() && !p.is_empty()), - env_cluster_role: std::env::var("STALWART_ROLE") + env_cluster_role: types::branding::env_var("ROLE") .ok() .filter(|r| !r.is_empty()), - env_push_shard_id: std::env::var("STALWART_PUSH_SHARD") + env_push_shard_id: types::branding::env_var("PUSH_SHARD") .ok() .and_then(|id| id.parse::().ok().and_then(|v| v.checked_sub(1))) .unwrap_or(0), - env_public_url: std::env::var("STALWART_PUBLIC_URL") + env_public_url: types::branding::env_var("PUBLIC_URL") .ok() .map(|v| v.trim().trim_end_matches('/').to_string()) .filter(|u| !u.is_empty()) .or_else(|| { - std::env::var("STALWART_HTTPS_PORT").ok().and_then(|p| { + types::branding::env_var("HTTPS_PORT").ok().and_then(|p| { p.parse::() .ok() .map(|port| format!("https://{}:{}", env_hostname, port)) diff --git a/crates/types/src/branding.rs b/crates/types/src/branding.rs index c6d1680..ecd896f 100644 --- a/crates/types/src/branding.rs +++ b/crates/types/src/branding.rs @@ -45,3 +45,22 @@ macro_rules! brand_url { "https://inbuxa.org" }; } + +/// Reads one of the server's environment variables by its unprefixed name, +/// such as `RECOVERY_ADMIN`. +/// +/// `INBUXA_` wins. `STALWART_` is still read when the new name +/// isn't set, so an existing Stalwart install moves over without editing its +/// environment, and a warning says which variable to rename. +pub fn env_var(name: &str) -> Result { + match std::env::var(format!("INBUXA_{name}")) { + Err(std::env::VarError::NotPresent) => { + let legacy = std::env::var(format!("STALWART_{name}")); + if legacy.is_ok() { + eprintln!("Warning: STALWART_{name} is deprecated; set INBUXA_{name} instead."); + } + legacy + } + found => found, + } +} diff --git a/crates/utils/src/http.rs b/crates/utils/src/http.rs index 6f78478..ccaf3b0 100644 --- a/crates/utils/src/http.rs +++ b/crates/utils/src/http.rs @@ -145,7 +145,7 @@ pub fn build_http_client( allow_invalid_certs: bool, ) -> Result { let mut headers = build_http_headers(raw_headers, username, password, token, content_type)?; - headers.insert(USER_AGENT, "Stalwart/1.0.0".parse().unwrap()); + headers.insert(USER_AGENT, "INBUXA/1.0.0".parse().unwrap()); // types::brand!(); utils does not depend on types match http_client_builder(allow_invalid_certs) .connect_timeout(timeout) diff --git a/docs/spec/SPEC.md b/docs/spec/SPEC.md index a2c0206..9cffaa1 100644 --- a/docs/spec/SPEC.md +++ b/docs/spec/SPEC.md @@ -103,10 +103,12 @@ repository. Instead: - Each import's full strip report is committed on `main` under `docs/fork/strip-reports/.md` (and `.json`), beside the merge that brought the release in. -- The snapshot includes upstream's `.github/` workflows, release automation - included. They're kept on `upstream` as upstream shipped them, but must be - disabled or replaced on `main` before the repository is ever pushed - anywhere that runs them. +- The snapshot includes upstream's `.github/`: its CI and release workflows, + workflows that auto-close issues and PRs from anyone not on its allowlist, + issue templates and Dependabot. On `main` the whole directory is moved to + `.github-upstream/`, so GitHub never runs it. Upstream changes to it still + merge there on each sync. INBUXA writes its own `.github/` when the + repository is first published. ### 2.2b What the first import proved (v0.16.22, 2026-09-18) @@ -179,6 +181,32 @@ one edition. interoperability, not branding, and renaming them breaks every existing client. Anything the fork adds uses its own namespace (open: which one). +### 2.5 Packaging + +Done 2026-09-18: + +- The package and binary are `inbuxa` (`cargo build -p inbuxa`). The binary's + help, banner and every protocol greeting say INBUXA (the branding module, + `types::brand!()`). +- Settings come from `INBUXA_*` environment variables. Each still falls back + to its `STALWART_*` name, with a startup warning to rename it + (`types::branding::env_var`). That covers all nine the server reads: + `HOSTNAME`, `RECOVERY_MODE`, `RECOVERY_ADMIN`, `RECOVERY_MODE_PORT`, + `RECOVERY_MODE_LOG_LEVEL`, `ROLE`, `PUSH_SHARD`, `PUBLIC_URL`, `HTTPS_PORT`. +- **Not renamed, on purpose:** `STALWART_APP_` and the two `STALWART_SPAM_...` + names. They look like environment variables, but they're keys inside the + data store, so renaming them would orphan existing installed apps and + spam-classifier models. +- New installs default to `/var/lib/inbuxa` for data and `/var/log/inbuxa` for + logs. Existing installs keep the paths their configuration names, so no data + moves. +- The container image runs as user `inbuxa` (uid 2000, as upstream), with + `/etc/inbuxa` and `/var/lib/inbuxa` as volumes, `INBUXA_HEALTHCHECK_URL`, and + `inbuxa --config /etc/inbuxa/config.json`. The systemd unit + (`inbuxa.service`), launchd plist and AppArmor profile are renamed to match. +- `install.sh` is a stub that says there's no release yet. Upstream's version + would download and install Stalwart itself. + ## 3. Clean room INBUXA runs on a paid Stalwart Enterprise license, so its maintainer is a diff --git a/install.sh b/install.sh index e362be2..43800d8 100644 --- a/install.sh +++ b/install.sh @@ -1,1069 +1,12 @@ #!/usr/bin/env sh -# shellcheck shell=dash - +# SPDX-FileCopyrightText: 2026 John Coffey +# SPDX-License-Identifier: AGPL-3.0-only # -# SPDX-FileCopyrightText: 2020 Stalwart Labs LLC -# -# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL -# - -# Stalwart install script -- based on the rustup installation script. - -set -e -set -u - -readonly BASE_URL="https://github.com/stalwartlabs/stalwart/releases/latest/download" - -main() { - downloader --check - need_cmd uname - need_cmd mktemp - need_cmd chmod - need_cmd chown - need_cmd mkdir - need_cmd rm - need_cmd tar - need_cmd cp - need_cmd hostname - - # Require root - if [ "$(id -u)" -ne 0 ]; then - err "❌ Install failed: This program needs to run as root." - fi - - # Detect OS - local _os _uname _account - _uname="$(uname)" - case "$_uname" in - Linux) _os="linux"; _account="stalwart" ;; - Darwin) _os="macos"; _account="_stalwart" ;; - FreeBSD) _os="freebsd"; _account="stalwart" ;; - *) err "❌ Install failed: Unsupported OS: $_uname" ;; - esac - - # Parse arguments - local _component="stalwart" - local _prefix="" - while [ $# -gt 0 ]; do - case "$1" in - --fdb) - _component="stalwart-foundationdb" - ;; - -h|--help) - print_usage - exit 0 - ;; - --*|-*) - err "❌ Unknown flag: $1 (try --help)" - ;; - *) - if [ -n "$_prefix" ]; then - err "❌ Only one prefix argument is allowed, got: $_prefix $1" - fi - _prefix="$1" - ;; - esac - shift - done - - # Derive install paths — FHS by default, self-contained under a custom prefix - local _bin_dir _bin_file _conf_dir _log_dir _data_dir _env_file _config_file - if [ -z "$_prefix" ]; then - _bin_dir="/usr/local/bin" - _log_dir="/var/log/stalwart" - if [ "$_os" = "freebsd" ]; then - # hier(7): third-party config lives under /usr/local/etc, - # variable data under /var/db - _conf_dir="/usr/local/etc/stalwart" - _data_dir="/var/db/stalwart" - else - _conf_dir="/etc/stalwart" - _data_dir="/var/lib/stalwart" - fi - else - _bin_dir="${_prefix}/bin" - _conf_dir="${_prefix}/etc" - _log_dir="${_prefix}/logs" - _data_dir="${_prefix}/data" - fi - _bin_file="${_bin_dir}/stalwart" - _config_file="${_conf_dir}/config.json" - _env_file="${_conf_dir}/stalwart.env" - - # Detect architecture - get_architecture || return 1 - local _arch="$RETVAL" - assert_nz "$_arch" "arch" - - # Create service account - create_account "$_os" "$_account" - - # Create directories - ensure mkdir -p "$_bin_dir" "$_conf_dir" "$_log_dir" "$_data_dir" - - # Download and install the binary - say "⏳ Downloading ${_component} for ${_arch}..." - local _tmp _tar _src_name - _tmp="$(mktemp -d)" - _tar="${_tmp}/stalwart.tar.gz" - ensure downloader "${BASE_URL}/${_component}-${_arch}.tar.gz" "$_tar" "$_arch" - ensure tar zxf "$_tar" -C "$_tmp" - _src_name="stalwart" - if [ "$_component" = "stalwart-foundationdb" ]; then - _src_name="stalwart-foundationdb" - fi - ensure cp "${_tmp}/${_src_name}" "$_bin_file" - ensure chmod 0755 "$_bin_file" - ensure rm -rf "$_tmp" - - # Create env file if absent (preserve user edits on reinstall) - if [ ! -e "$_env_file" ]; then - say "📝 Writing env file at ${_env_file}..." - write_env_file "$_env_file" - fi - - # Ownership and permissions - say "🔐 Setting permissions..." - ensure chown "${_account}:${_account}" "$_conf_dir" "$_log_dir" "$_data_dir" - ensure chmod 0750 "$_conf_dir" "$_log_dir" "$_data_dir" - ensure chown "root:${_account}" "$_env_file" - ensure chmod 0640 "$_env_file" - - # Install and start the service - say "🚀 Starting service..." - local _service_type="" - case "$_os" in - linux) - if check_cmd systemctl; then - create_service_linux_systemd "$_bin_file" "$_config_file" "$_env_file" "$_account" - _service_type="systemd" - else - create_service_linux_initd "$_bin_file" "$_config_file" "$_env_file" "$_account" - _service_type="initd" - fi - ;; - macos) - create_service_macos "$_bin_file" "$_config_file" "$_env_file" "$_account" - _service_type="launchd" - ;; - freebsd) - create_service_freebsd "$_bin_file" "$_config_file" "$_env_file" "$_account" "$_log_dir" - _service_type="rcd" - ;; - esac - - # Completion message - local _host - _host="$(hostname -f 2>/dev/null || hostname)" - say "" - say "🎉 Installation complete!" - say "" - say "Stalwart is running in bootstrap mode. A temporary administrator" - say "password was generated at startup and printed to the service logs." - say "" - say "👉 To find the password, inspect the service logs:" - case "$_service_type" in - systemd) - say " journalctl -u stalwart -n 200 | grep -A8 'bootstrap mode'" - ;; - initd) - say " grep -A8 'bootstrap mode' /var/log/syslog 2>/dev/null \\" - say " || grep -A8 'bootstrap mode' /var/log/messages" - ;; - launchd) - say " sudo log show --predicate 'process == \"stalwart\"' --last 5m" - ;; - rcd) - say " grep -A8 'bootstrap mode' ${_log_dir}/stalwart.log" - ;; - esac - say "" - say " Or set STALWART_RECOVERY_ADMIN=admin: in" - say " ${_env_file} and restart the service to pin a credential." - say "" - say " Finish setup at: http://${_host}:8080/admin" - say "" - - return 0 -} - -print_usage() { - cat <<'EOF' -Usage: install.sh [--fdb] [PREFIX] - -Install Stalwart into standard FHS paths or under a custom prefix. - -Options: - --fdb Install the FoundationDB build. - -h, --help Show this help. - -With no PREFIX, Stalwart is installed under standard FHS paths: - binary /usr/local/bin/stalwart - config /etc/stalwart/config.json (/usr/local/etc/stalwart/config.json on FreeBSD) - env /etc/stalwart/stalwart.env (/usr/local/etc/stalwart/stalwart.env on FreeBSD) - logs /var/log/stalwart/ - data /var/lib/stalwart/ (/var/db/stalwart on FreeBSD) - -When PREFIX is provided, a self-contained layout is used instead: - binary $PREFIX/bin/stalwart - config $PREFIX/etc/config.json - env $PREFIX/etc/stalwart.env - logs $PREFIX/logs/ - data $PREFIX/data/ -EOF -} - -write_env_file() { - cat > "$1" <<'EOF' -# Environment variables for the Stalwart service. -# Uncomment and edit an entry to override its default. - -# Override the hostname used in HTTP responses -#STALWART_HOSTNAME=mail.example.com - -# Override the public base URL published in OAuth, OIDC, and JMAP discovery -# documents. Accepts scheme, host, optional port, and optional path prefix. -#STALWART_PUBLIC_URL=https://mail.example.com - -# Enable bootstrap / recovery mode on startup. Accepted: 1, true. Default: false. -#STALWART_RECOVERY_MODE=true - -# Log level while in recovery mode. Default: info. -#STALWART_RECOVERY_MODE_LOG_LEVEL=debug - -# HTTP port used in recovery mode. Default: 8080. -#STALWART_RECOVERY_MODE_PORT=9090 - -# Fixed administrator credentials — format: username:password -# Default: a temporary random password is generated and printed to the logs. -#STALWART_RECOVERY_ADMIN=admin:changeme - -# Cluster role assigned to this node. Must match a role name defined in the -# cluster registry. Leave unset for a standalone (non-clustered) deployment. -#STALWART_ROLE=primary - -# Push-notification shard this node is responsible for, when running in a -# cluster. -#STALWART_PUSH_SHARD=1 -EOF -} - -create_account() { - local _os="$1" - local _account="$2" - if id -u "$_account" > /dev/null 2>&1; then - return 0 - fi - say "🖥️ Creating '${_account}' account..." - if [ "$_os" = "macos" ]; then - local _last_uid _last_gid _uid _gid - _last_uid="$(dscacheutil -q user | grep uid | awk '{print $2}' | sort -n | tail -n 1)" - _last_gid="$(dscacheutil -q group | grep gid | awk '{print $2}' | sort -n | tail -n 1)" - _uid="$((_last_uid+1))" - _gid="$((_last_gid+1))" - - ensure dscl /Local/Default -create Groups/_stalwart - ensure dscl /Local/Default -create Groups/_stalwart Password \* - ensure dscl /Local/Default -create Groups/_stalwart PrimaryGroupID $_gid - ensure dscl /Local/Default -create Groups/_stalwart RealName "Stalwart service" - ensure dscl /Local/Default -create Groups/_stalwart RecordName _stalwart stalwart - - ensure dscl /Local/Default -create Users/_stalwart - ensure dscl /Local/Default -create Users/_stalwart NFSHomeDirectory /var/empty - ensure dscl /Local/Default -create Users/_stalwart Password \* - ensure dscl /Local/Default -create Users/_stalwart PrimaryGroupID $_gid - ensure dscl /Local/Default -create Users/_stalwart RealName "Stalwart service" - ensure dscl /Local/Default -create Users/_stalwart RecordName _stalwart stalwart - ensure dscl /Local/Default -create Users/_stalwart UniqueID $_uid - ensure dscl /Local/Default -create Users/_stalwart UserShell /usr/bin/false - - ensure dscl /Local/Default -delete /Users/_stalwart AuthenticationAuthority - ensure dscl /Local/Default -delete /Users/_stalwart PasswordPolicyOptions - elif [ "$_os" = "freebsd" ]; then - ensure pw useradd -n "$_account" -c "Stalwart service" -d /nonexistent -s /usr/sbin/nologin -w no - else - ensure useradd "$_account" -s /usr/sbin/nologin -M -r -U - fi -} - -create_service_linux_systemd() { - local _bin="$1" _config="$2" _env="$3" _user="$4" - cat > /etc/systemd/system/stalwart.service < /etc/init.d/stalwart < /dev/null \\ - || return 1 - start-stop-daemon --start --quiet --pidfile \$PIDFILE --exec \$DAEMON \\ - --background --make-pidfile --chuid ${_user}:${_user} \\ - -- \$DAEMON_ARGS \\ - || return 2 -} - -do_stop() -{ - start-stop-daemon --stop --quiet --retry=INT/30/KILL/5 --pidfile \$PIDFILE --name stalwart - RETVAL="\$?" - [ "\$RETVAL" = 2 ] && return 2 - start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec \$DAEMON - [ "\$?" = 2 ] && return 2 - rm -f \$PIDFILE - return "\$RETVAL" -} - -case "\$1" in - start) - [ "\$VERBOSE" != no ] && log_daemon_msg "Starting Stalwart Server" "stalwart" - do_start - case "\$?" in - 0|1) [ "\$VERBOSE" != no ] && log_end_msg 0 ;; - 2) [ "\$VERBOSE" != no ] && log_end_msg 1 ;; - esac - ;; - stop) - [ "\$VERBOSE" != no ] && log_daemon_msg "Stopping Stalwart Server" "stalwart" - do_stop - case "\$?" in - 0|1) [ "\$VERBOSE" != no ] && log_end_msg 0 ;; - 2) [ "\$VERBOSE" != no ] && log_end_msg 1 ;; - esac - ;; - status) - status_of_proc "\$DAEMON" "stalwart" && exit 0 || exit \$? - ;; - restart) - log_daemon_msg "Restarting Stalwart Server" "stalwart" - do_stop - case "\$?" in - 0|1) - do_start - case "\$?" in - 0) log_end_msg 0 ;; - *) log_end_msg 1 ;; - esac - ;; - *) - log_end_msg 1 - ;; - esac - ;; - *) - echo "Usage: /etc/init.d/stalwart {start|stop|status|restart}" >&2 - exit 3 - ;; -esac - -exit 0 -EOF - chmod +x /etc/init.d/stalwart - update-rc.d stalwart defaults - service stalwart start -} - -create_service_macos() { - local _bin="$1" _config="$2" _env="$3" _user="$4" - local _plist="/Library/LaunchDaemons/stalwart.plist" - - # Remove any legacy LaunchDaemons from a prior install - if [ -f "$_plist" ]; then - launchctl bootout system/ "$_plist" 2>/dev/null || true - rm -f "$_plist" - fi - - # launchd has no EnvironmentFile equivalent — wrap with sh to source the env file - cat > "$_plist" < - - - - Label - stalwart - ServiceDescription - Stalwart - UserName - ${_user} - GroupName - ${_user} - ProgramArguments - - /bin/sh - -c - set -a; if [ -r "${_env}" ]; then . "${_env}"; fi; set +a; exec "${_bin}" --config="${_config}" - - RunAtLoad - - KeepAlive - - - -EOF - chmod 0644 "$_plist" - chown root:wheel "$_plist" - launchctl bootout system/ "$_plist" 2>/dev/null || true - launchctl bootstrap system/ "$_plist" -} - -create_service_freebsd() { - local _bin="$1" _config="$2" _env="$3" _user="$4" _log_dir="$5" - ensure mkdir -p /usr/local/etc/rc.d - cat > /usr/local/etc/rc.d/stalwart < /dev/null 2>&1 || true - service stalwart start -} - - -get_architecture() { - local _ostype _cputype _bitness _arch _clibtype - _ostype="$(uname -s)" - _cputype="$(uname -m)" - _clibtype="gnu" - - if [ "$_ostype" = Linux ]; then - if [ "$(uname -o)" = Android ]; then - _ostype=Android - fi - if ldd --version 2>&1 | grep -q 'musl'; then - _clibtype="musl" - fi - fi - - if [ "$_ostype" = Darwin ] && [ "$_cputype" = i386 ]; then - # Darwin `uname -m` lies - if sysctl hw.optional.x86_64 | grep -q ': 1'; then - _cputype=x86_64 - fi - fi - - if [ "$_ostype" = SunOS ]; then - # Both Solaris and illumos presently announce as "SunOS" in "uname -s" - # so use "uname -o" to disambiguate. We use the full path to the - # system uname in case the user has coreutils uname first in PATH, - # which has historically sometimes printed the wrong value here. - if [ "$(/usr/bin/uname -o)" = illumos ]; then - _ostype=illumos - fi - - # illumos systems have multi-arch userlands, and "uname -m" reports the - # machine hardware name; e.g., "i86pc" on both 32- and 64-bit x86 - # systems. Check for the native (widest) instruction set on the - # running kernel: - if [ "$_cputype" = i86pc ]; then - _cputype="$(isainfo -n)" - fi - fi - - case "$_ostype" in - - Android) - _ostype=linux-android - ;; - - Linux) - check_proc - _ostype=unknown-linux-$_clibtype - _bitness=$(get_bitness) - ;; - - FreeBSD) - _ostype=unknown-freebsd - ;; - - NetBSD) - _ostype=unknown-netbsd - ;; - - DragonFly) - _ostype=unknown-dragonfly - ;; - - Darwin) - _ostype=apple-darwin - ;; - - illumos) - _ostype=unknown-illumos - ;; - - MINGW* | MSYS* | CYGWIN* | Windows_NT) - _ostype=pc-windows-gnu - ;; - - *) - err "unrecognized OS type: $_ostype" - ;; - - esac - - case "$_cputype" in - - i386 | i486 | i686 | i786 | x86) - _cputype=i686 - ;; - - xscale | arm) - _cputype=arm - if [ "$_ostype" = "linux-android" ]; then - _ostype=linux-androideabi - fi - ;; - - armv6l) - _cputype=arm - if [ "$_ostype" = "linux-android" ]; then - _ostype=linux-androideabi - else - _ostype="${_ostype}eabihf" - fi - ;; - - armv7l | armv8l) - _cputype=armv7 - if [ "$_ostype" = "linux-android" ]; then - _ostype=linux-androideabi - else - _ostype="${_ostype}eabihf" - fi - ;; - - aarch64 | arm64) - _cputype=aarch64 - ;; - - x86_64 | x86-64 | x64 | amd64) - _cputype=x86_64 - ;; - - mips) - _cputype=$(get_endianness mips '' el) - ;; - - mips64) - if [ "$_bitness" -eq 64 ]; then - # only n64 ABI is supported for now - _ostype="${_ostype}abi64" - _cputype=$(get_endianness mips64 '' el) - fi - ;; - - ppc) - _cputype=powerpc - ;; - - ppc64) - _cputype=powerpc64 - ;; - - ppc64le) - _cputype=powerpc64le - ;; - - s390x) - _cputype=s390x - ;; - riscv64) - _cputype=riscv64gc - ;; - *) - err "unknown CPU type: $_cputype" - - esac - - # Detect 64-bit linux with 32-bit userland - if [ "${_ostype}" = unknown-linux-gnu ] && [ "${_bitness}" -eq 32 ]; then - case $_cputype in - x86_64) - if [ -n "${RUSTUP_CPUTYPE:-}" ]; then - _cputype="$RUSTUP_CPUTYPE" - else { - # 32-bit executable for amd64 = x32 - if is_host_amd64_elf; then { - echo "This host is running an x32 userland; as it stands, x32 support is poor," 1>&2 - echo "and there isn't a native toolchain -- you will have to install" 1>&2 - echo "multiarch compatibility with i686 and/or amd64, then select one" 1>&2 - echo "by re-running this script with the RUSTUP_CPUTYPE environment variable" 1>&2 - echo "set to i686 or x86_64, respectively." 1>&2 - echo 1>&2 - echo "You will be able to add an x32 target after installation by running" 1>&2 - echo " rustup target add x86_64-unknown-linux-gnux32" 1>&2 - exit 1 - }; else - _cputype=i686 - fi - }; fi - ;; - mips64) - _cputype=$(get_endianness mips '' el) - ;; - powerpc64) - _cputype=powerpc - ;; - aarch64) - _cputype=armv7 - if [ "$_ostype" = "linux-android" ]; then - _ostype=linux-androideabi - else - _ostype="${_ostype}eabihf" - fi - ;; - riscv64gc) - err "riscv64 with 32-bit userland unsupported" - ;; - esac - fi - - # Detect armv7 but without the CPU features Rust needs in that build, - # and fall back to arm. - # See https://github.com/rust-lang/rustup.rs/issues/587. - if [ "$_ostype" = "unknown-linux-gnueabihf" ] && [ "$_cputype" = armv7 ]; then - if ensure grep '^Features' /proc/cpuinfo | grep -q -v neon; then - # At least one processor does not have NEON. - _cputype=arm - fi - fi - - _arch="${_cputype}-${_ostype}" - - RETVAL="$_arch" -} - -check_proc() { - # Check for /proc by looking for the /proc/self/exe link - # This is only run on Linux - if ! test -L /proc/self/exe ; then - err "fatal: Unable to find /proc/self/exe. Is /proc mounted? Installation cannot proceed without /proc." - fi -} - -get_bitness() { - need_cmd head - # Architecture detection without dependencies beyond coreutils. - # ELF files start out "\x7fELF", and the following byte is - # 0x01 for 32-bit and - # 0x02 for 64-bit. - # The printf builtin on some shells like dash only supports octal - # escape sequences, so we use those. - local _current_exe_head - _current_exe_head=$(head -c 5 /proc/self/exe ) - if [ "$_current_exe_head" = "$(printf '\177ELF\001')" ]; then - echo 32 - elif [ "$_current_exe_head" = "$(printf '\177ELF\002')" ]; then - echo 64 - else - err "unknown platform bitness" - fi -} - -is_host_amd64_elf() { - need_cmd head - need_cmd tail - # ELF e_machine detection without dependencies beyond coreutils. - # Two-byte field at offset 0x12 indicates the CPU, - # but we're interested in it being 0x3E to indicate amd64, or not that. - local _current_exe_machine - _current_exe_machine=$(head -c 19 /proc/self/exe | tail -c 1) - [ "$_current_exe_machine" = "$(printf '\076')" ] -} - -get_endianness() { - local cputype=$1 - local suffix_eb=$2 - local suffix_el=$3 - - # detect endianness without od/hexdump, like get_bitness() does. - need_cmd head - need_cmd tail - - local _current_exe_endianness - _current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)" - if [ "$_current_exe_endianness" = "$(printf '\001')" ]; then - echo "${cputype}${suffix_el}" - elif [ "$_current_exe_endianness" = "$(printf '\002')" ]; then - echo "${cputype}${suffix_eb}" - else - err "unknown platform endianness" - fi -} - -say() { - printf '%s\n' "$1" -} - -err() { - say "$1" >&2 - exit 1 -} - -need_cmd() { - if ! check_cmd "$1"; then - err "need '$1' (command not found)" - fi -} - -check_cmd() { - command -v "$1" > /dev/null 2>&1 -} - -assert_nz() { - if [ -z "$1" ]; then err "assert_nz $2"; fi -} - -# Run a command that should never fail. If the command fails execution -# will immediately terminate with an error showing the failing -# command. -ensure() { - if ! "$@"; then err "command failed: $*"; fi -} - -# This wraps curl or wget. Try curl first, if not installed, -# use wget instead. -downloader() { - local _dld - local _ciphersuites - local _err - local _status - local _retry - if check_cmd curl; then - _dld=curl - elif check_cmd wget; then - _dld=wget - else - _dld='curl or wget' # to be used in error message of need_cmd - fi - - if [ "$1" = --check ]; then - need_cmd "$_dld" - elif [ "$_dld" = curl ]; then - check_curl_for_retry_support - _retry="$RETVAL" - get_ciphersuites_for_curl - _ciphersuites="$RETVAL" - if [ -n "$_ciphersuites" ]; then - _err=$(curl $_retry --proto '=https' --tlsv1.2 --ciphers "$_ciphersuites" --silent --show-error --fail --location "$1" --output "$2" 2>&1) - _status=$? - else - echo "Warning: Not enforcing strong cipher suites for TLS, this is potentially less secure" - if ! check_help_for "$3" curl --proto --tlsv1.2; then - echo "Warning: Not enforcing TLS v1.2, this is potentially less secure" - _err=$(curl $_retry --silent --show-error --fail --location "$1" --output "$2" 2>&1) - _status=$? - else - _err=$(curl $_retry --proto '=https' --tlsv1.2 --silent --show-error --fail --location "$1" --output "$2" 2>&1) - _status=$? - fi - fi - if [ -n "$_err" ]; then - if echo "$_err" | grep -q 404; then - err "❌ Binary for platform '$3' not found, this platform may be unsupported." - else - echo "$_err" >&2 - fi - fi - return $_status - elif [ "$_dld" = wget ]; then - if [ "$(wget -V 2>&1|head -2|tail -1|cut -f1 -d" ")" = "BusyBox" ]; then - echo "Warning: using the BusyBox version of wget. Not enforcing strong cipher suites for TLS or TLS v1.2, this is potentially less secure" - _err=$(wget "$1" -O "$2" 2>&1) - _status=$? - else - get_ciphersuites_for_wget - _ciphersuites="$RETVAL" - if [ -n "$_ciphersuites" ]; then - _err=$(wget --https-only --secure-protocol=TLSv1_2 --ciphers "$_ciphersuites" "$1" -O "$2" 2>&1) - _status=$? - else - echo "Warning: Not enforcing strong cipher suites for TLS, this is potentially less secure" - if ! check_help_for "$3" wget --https-only --secure-protocol; then - echo "Warning: Not enforcing TLS v1.2, this is potentially less secure" - _err=$(wget "$1" -O "$2" 2>&1) - _status=$? - else - _err=$(wget --https-only --secure-protocol=TLSv1_2 "$1" -O "$2" 2>&1) - _status=$? - fi - fi - fi - if [ -n "$_err" ]; then - if echo "$_err" | grep -q ' 404 Not Found'; then - err "❌ Binary for platform '$3' not found, this platform may be unsupported." - else - echo "$_err" >&2 - fi - fi - return $_status - else - err "Unknown downloader" # should not reach here - fi -} - -# Check if curl supports the --retry flag, then pass it to the curl invocation. -check_curl_for_retry_support() { - local _retry_supported="" - # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. - if check_help_for "notspecified" "curl" "--retry"; then - _retry_supported="--retry 3" - fi - - RETVAL="$_retry_supported" - -} - -check_help_for() { - local _arch - local _cmd - local _arg - _arch="$1" - shift - _cmd="$1" - shift - - local _category - if "$_cmd" --help | grep -q 'For all options use the manual or "--help all".'; then - _category="all" - else - _category="" - fi - - case "$_arch" in - - *darwin*) - if check_cmd sw_vers; then - case $(sw_vers -productVersion) in - 10.*) - # If we're running on macOS, older than 10.13, then we always - # fail to find these options to force fallback - if [ "$(sw_vers -productVersion | cut -d. -f2)" -lt 13 ]; then - # Older than 10.13 - echo "Warning: Detected macOS platform older than 10.13" - return 1 - fi - ;; - 11.*) - # We assume Big Sur will be OK for now - ;; - *) - # Unknown product version, warn and continue - echo "Warning: Detected unknown macOS major version: $(sw_vers -productVersion)" - echo "Warning TLS capabilities detection may fail" - ;; - esac - fi - ;; - - esac - - for _arg in "$@"; do - if ! "$_cmd" --help $_category | grep -q -- "$_arg"; then - return 1 - fi - done - - true # not strictly needed -} - -# Return cipher suite string specified by user, otherwise return strong TLS 1.2-1.3 cipher suites -# if support by local tools is detected. Detection currently supports these curl backends: -# GnuTLS and OpenSSL (possibly also LibreSSL and BoringSSL). Return value can be empty. -get_ciphersuites_for_curl() { - if [ -n "${RUSTUP_TLS_CIPHERSUITES-}" ]; then - # user specified custom cipher suites, assume they know what they're doing - RETVAL="$RUSTUP_TLS_CIPHERSUITES" - return - fi - - local _openssl_syntax="no" - local _gnutls_syntax="no" - local _backend_supported="yes" - if curl -V | grep -q ' OpenSSL/'; then - _openssl_syntax="yes" - elif curl -V | grep -iq ' LibreSSL/'; then - _openssl_syntax="yes" - elif curl -V | grep -iq ' BoringSSL/'; then - _openssl_syntax="yes" - elif curl -V | grep -iq ' GnuTLS/'; then - _gnutls_syntax="yes" - else - _backend_supported="no" - fi - - local _args_supported="no" - if [ "$_backend_supported" = "yes" ]; then - # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. - if check_help_for "notspecified" "curl" "--tlsv1.2" "--ciphers" "--proto"; then - _args_supported="yes" - fi - fi - - local _cs="" - if [ "$_args_supported" = "yes" ]; then - if [ "$_openssl_syntax" = "yes" ]; then - _cs=$(get_strong_ciphersuites_for "openssl") - elif [ "$_gnutls_syntax" = "yes" ]; then - _cs=$(get_strong_ciphersuites_for "gnutls") - fi - fi - - RETVAL="$_cs" -} - -# Return cipher suite string specified by user, otherwise return strong TLS 1.2-1.3 cipher suites -# if support by local tools is detected. Detection currently supports these wget backends: -# GnuTLS and OpenSSL (possibly also LibreSSL and BoringSSL). Return value can be empty. -get_ciphersuites_for_wget() { - if [ -n "${RUSTUP_TLS_CIPHERSUITES-}" ]; then - # user specified custom cipher suites, assume they know what they're doing - RETVAL="$RUSTUP_TLS_CIPHERSUITES" - return - fi - - local _cs="" - if wget -V | grep -q '\-DHAVE_LIBSSL'; then - # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. - if check_help_for "notspecified" "wget" "TLSv1_2" "--ciphers" "--https-only" "--secure-protocol"; then - _cs=$(get_strong_ciphersuites_for "openssl") - fi - elif wget -V | grep -q '\-DHAVE_LIBGNUTLS'; then - # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. - if check_help_for "notspecified" "wget" "TLSv1_2" "--ciphers" "--https-only" "--secure-protocol"; then - _cs=$(get_strong_ciphersuites_for "gnutls") - fi - fi - - RETVAL="$_cs" -} - -# Return strong TLS 1.2-1.3 cipher suites in OpenSSL or GnuTLS syntax. TLS 1.2 -# excludes non-ECDHE and non-AEAD cipher suites. DHE is excluded due to bad -# DH params often found on servers (see RFC 7919). Sequence matches or is -# similar to Firefox 68 ESR with weak cipher suites disabled via about:config. -# $1 must be openssl or gnutls. -get_strong_ciphersuites_for() { - if [ "$1" = "openssl" ]; then - # OpenSSL is forgiving of unknown values, no problems with TLS 1.3 values on versions that don't support it yet. - echo "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384" - elif [ "$1" = "gnutls" ]; then - # GnuTLS isn't forgiving of unknown values, so this may require a GnuTLS version that supports TLS 1.3 even if wget doesn't. - # Begin with SECURE128 (and higher) then remove/add to build cipher suites. Produces same 9 cipher suites as OpenSSL but in slightly different order. - echo "SECURE128:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-DTLS-ALL:-CIPHER-ALL:-MAC-ALL:-KX-ALL:+AEAD:+ECDHE-ECDSA:+ECDHE-RSA:+AES-128-GCM:+CHACHA20-POLY1305:+AES-256-GCM" - fi -} - -# This is just for indicating that commands' results are being -# intentionally ignored. Usually, because it's being executed -# as part of error handling. -ignore() { - "$@" -} - -main "$@" || exit 1 +# Upstream's install.sh downloads Stalwart's release binaries from GitHub, so +# it would install Stalwart, not INBUXA. It's replaced with this notice until +# INBUXA publishes releases of its own. + +echo "INBUXA has no releases yet, so there is nothing to install." >&2 +echo "Build from source instead: cargo build --release -p inbuxa" >&2 +echo "See https://inbuxa.org once it's up." >&2 +exit 1 diff --git a/resources/apparmor.d/stalwart-mail b/resources/apparmor.d/inbuxa similarity index 88% rename from resources/apparmor.d/stalwart-mail rename to resources/apparmor.d/inbuxa index f563931..0066ed8 100644 --- a/resources/apparmor.d/stalwart-mail +++ b/resources/apparmor.d/inbuxa @@ -1,6 +1,6 @@ #include -profile stalwart flags=(attach_disconnected) { +profile inbuxa flags=(attach_disconnected) { #include #include #include @@ -17,8 +17,8 @@ profile stalwart flags=(attach_disconnected) { owner /proc/*/net/if_inet6 r, owner /proc/*/net/ipv6_route r, - # Full write access to /opt/stalwart - /opt/stalwart/** rwk, + # Full write access to /opt/inbuxa + /opt/inbuxa/** rwk, # Allow creating directories under /tmp /tmp/ r, @@ -51,9 +51,9 @@ profile stalwart flags=(attach_disconnected) { network inet6 dgram bind port 7911, # Basic system access - /usr/bin/stalwart rix, - /etc/stalwart/** r, - /var/log/stalwart/** w, + /usr/bin/inbuxa rix, + /etc/inbuxa/** r, + /var/log/inbuxa/** w, # Additional permissions might be needed depending on specific requirements } diff --git a/resources/systemd/stalwart.mail.plist b/resources/systemd/inbuxa.mail.plist similarity index 79% rename from resources/systemd/stalwart.mail.plist rename to resources/systemd/inbuxa.mail.plist index 15f5ab1..512f232 100644 --- a/resources/systemd/stalwart.mail.plist +++ b/resources/systemd/inbuxa.mail.plist @@ -4,12 +4,12 @@ Label - stalwart.mail + inbuxa.mail ServiceDescription - Stalwart + INBUXA ProgramArguments - __PATH__/bin/stalwart + __PATH__/bin/inbuxa --config=__PATH__/etc/config.json RunAtLoad diff --git a/resources/systemd/stalwart-mail.service b/resources/systemd/inbuxa.service similarity index 68% rename from resources/systemd/stalwart-mail.service rename to resources/systemd/inbuxa.service index 4e2cb46..b2e48df 100644 --- a/resources/systemd/stalwart-mail.service +++ b/resources/systemd/inbuxa.service @@ -1,5 +1,5 @@ [Unit] -Description=Stalwart Server +Description=INBUXA Server Conflicts=postfix.service sendmail.service exim4.service ConditionPathExists=__PATH__/etc/config.json After=network-online.target @@ -11,10 +11,10 @@ KillMode=process KillSignal=SIGINT Restart=on-failure RestartSec=5 -ExecStart=__PATH__/bin/stalwart --config=__PATH__/etc/config.json -SyslogIdentifier=stalwart -User=stalwart -Group=stalwart +ExecStart=__PATH__/bin/inbuxa --config=__PATH__/etc/config.json +SyslogIdentifier=inbuxa +User=inbuxa +Group=inbuxa AmbientCapabilities=CAP_NET_BIND_SERVICE [Install]