From a86df3a7abdfd969e3af3936ad2a78bf78d56e2b Mon Sep 17 00:00:00 2001 From: "Wyatt J. Miller" Date: Wed, 21 Oct 2026 19:17:30 -0400 Subject: [PATCH] add: stalwart package, nixos module ripped from nixpkgs, maintaining stalwart myself for a bit --- modules/nixos/default.nix | 3 + modules/nixos/stalwart.nix | 311 ++++++++++++++++++++++++++ pkgs/common/stalwart/spam-filter.nix | 54 +++++ pkgs/common/stalwart/stalwart.nix | 315 +++++++++++++++++++++++++++ pkgs/common/stalwart/webui.nix | 76 +++++++ pkgs/default.nix | 1 + 6 files changed, 760 insertions(+) create mode 100644 modules/nixos/stalwart.nix create mode 100644 pkgs/common/stalwart/spam-filter.nix create mode 100644 pkgs/common/stalwart/stalwart.nix create mode 100644 pkgs/common/stalwart/webui.nix diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix index e69de29..c2fe8b0 100644 --- a/modules/nixos/default.nix +++ b/modules/nixos/default.nix @@ -0,0 +1,3 @@ +self: { + stalwart = import ./stalwart.nix self; +} diff --git a/modules/nixos/stalwart.nix b/modules/nixos/stalwart.nix new file mode 100644 index 0000000..9cd5f1d --- /dev/null +++ b/modules/nixos/stalwart.nix @@ -0,0 +1,311 @@ +self: { + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.stalwart; + configFormat = pkgs.formats.toml { }; + configFile = configFormat.generate "stalwart.toml" cfg.settings; + useLegacyStorage = lib.versionOlder cfg.stateVersion "24.11"; + pre2605 = lib.versionOlder cfg.stateVersion "26.05"; + stalwartIdentifier = if pre2605 then "stalwart-mail" else "stalwart"; + stalwartIdentifierText = ''if lib.versionOlder config.services.stalwart.stateVersion "26.05" then "stalwart-mail" else "stalwart"''; + + parsePorts = + listeners: + let + parseAddresses = listeners: lib.flatten (lib.mapAttrsToList (name: value: value.bind) listeners); + splitAddress = addr: lib.splitString ":" addr; + extractPort = addr: lib.toInt (builtins.foldl' (a: b: b) "" (splitAddress addr)); + in + map (address: extractPort address) (parseAddresses listeners); + +in +{ + imports = [ + # since 0.12.0 (2025-05-26) release, upstream re-branded project to 'stalwart' due to inclusion of collaboration features (CalDAV, CardDAV, and WebDAV) + # https://github.com/stalwartlabs/stalwart/releases/tag/v0.12.0 + (lib.mkRenamedOptionModule [ "services" "stalwart-mail" ] [ "services" "stalwart" ]) + ]; + options.services.stalwart = { + enable = lib.mkEnableOption "the all-in-one collaboration and mail server, Stalwart"; + + stateVersion = lib.mkOption { + type = lib.types.str; + description = '' + The version of this module (=version of NixOS) when this module was first enabled on this particular machine, used to maintain compatibility with application data created on older versions of this module. + + See {option}`system.stateVersion` for details on the NixOS-global equivalent to this option. + ''; + }; + + package = lib.mkPackageOption pkgs "stalwart" { }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to open TCP firewall ports, which are specified in + {option}`services.stalwart.settings.server.listener` on all interfaces. + ''; + }; + + settings = lib.mkOption { + inherit (configFormat) type; + default = { }; + description = '' + Configuration options for the Stalwart server. + See for available options. + + By default, the module is configured to store everything locally. + ''; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/${stalwartIdentifier}"; + defaultText = lib.literalExpression "/var/lib/\${${stalwartIdentifierText}}"; + description = '' + Data directory for stalwart + ''; + }; + + user = lib.mkOption { + type = lib.types.str; + default = stalwartIdentifier; + defaultText = lib.literalExpression stalwartIdentifierText; + description = '' + User ownership of service + ''; + }; + + group = lib.mkOption { + type = lib.types.str; + default = stalwartIdentifier; + defaultText = lib.literalExpression stalwartIdentifierText; + description = '' + Group ownership of service + ''; + }; + + credentials = lib.mkOption { + description = '' + Credentials envs used to configure Stalwart secrets. + These secrets can be accessed in configuration values with + the macros such as + `%{file:/run/credentials/stalwart.service/VAR_NAME}%`. + ''; + type = lib.types.attrsOf lib.types.str; + default = { }; + example = { + user_admin_password = "/run/keys/stalwart_admin_password"; + }; + }; + + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = + !( + (lib.hasAttrByPath [ "settings" "queue" ] cfg) + && (builtins.any (lib.hasAttrByPath [ + "value" + "next-hop" + ]) (lib.attrsToList cfg.settings.queue)) + ); + message = '' + Stalwart deprecated `next-hop` in favor of "virtual queues" `queue.strategy.route` \ + with v0.13.0 see [Outbound Strategy](https://stalw.art/docs/mta/outbound/strategy/#configuration) \ + and [release announcement](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md#upgrading-from-v012x-and-v011x-to-v013x). + ''; + } + ]; + + # Default config: all local + services.stalwart.settings = { + tracer = + if pre2605 then + { + stdout = { + type = lib.mkDefault "stdout"; + level = lib.mkDefault "info"; + ansi = lib.mkDefault false; # no colour markers to journald + enable = lib.mkDefault true; + }; + } + else + { + journal = { + type = lib.mkDefault "journal"; + level = lib.mkDefault "info"; + enable = lib.mkDefault true; + }; + }; + store = + if useLegacyStorage then + { + # structured data in SQLite, blobs on filesystem + db.type = lib.mkDefault "sqlite"; + db.path = lib.mkDefault "${cfg.dataDir}/data/index.sqlite3"; + fs.type = lib.mkDefault "fs"; + fs.path = lib.mkDefault "${cfg.dataDir}/data/blobs"; + } + else + { + # everything in RocksDB + db.type = lib.mkDefault "rocksdb"; + db.path = lib.mkDefault "${cfg.dataDir}/db"; + db.compression = lib.mkDefault "lz4"; + }; + storage.data = lib.mkDefault "db"; + storage.fts = lib.mkDefault "db"; + storage.lookup = lib.mkDefault "db"; + storage.blob = lib.mkDefault (if useLegacyStorage then "fs" else "db"); + directory.internal.type = lib.mkDefault "internal"; + directory.internal.store = lib.mkDefault "db"; + storage.directory = lib.mkDefault "internal"; + resolver.type = lib.mkDefault "system"; + resolver.public-suffix = lib.mkDefault [ + "file://${pkgs.publicsuffix-list}/share/publicsuffix/public_suffix_list.dat" + ]; + spam-filter.resource = lib.mkDefault "file://${cfg.package.spam-filter}/spam-filter.toml"; + webadmin = + let + hasHttpListener = builtins.any (listener: listener.protocol == "http") ( + lib.attrValues (cfg.settings.server.listener or { }) + ); + in + { + path = "/var/cache/${stalwartIdentifier}"; + resource = lib.mkIf hasHttpListener (lib.mkDefault "file://${cfg.package.webadmin}/webadmin.zip"); + }; + }; + + # This service stores a potentially large amount of data. + # Running it as a dynamic user would force chown to be run everytime the + # service is restarted on a potentially large number of files. + # That would cause unnecessary and unwanted delays. + users = { + groups = lib.mkIf (cfg.group == stalwartIdentifier) { + ${cfg.group} = { }; + }; + users = lib.mkIf (cfg.user == stalwartIdentifier) { + ${cfg.user} = { + isSystemUser = true; + inherit (cfg) group; + }; + }; + }; + + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' - '${cfg.user}' '${cfg.group}' - -" + ]; + + systemd = { + services.stalwart = { + description = "Stalwart Server"; + wantedBy = [ "multi-user.target" ]; + after = [ + "local-fs.target" + "network.target" + ]; + + serviceConfig = { + # Upstream service config + Type = "simple"; + LimitNOFILE = 65536; + KillMode = "process"; + KillSignal = "SIGINT"; + Restart = "on-failure"; + RestartSec = 5; + SyslogIdentifier = stalwartIdentifier; + + ExecStartPre = + if useLegacyStorage then + '' + ${lib.getExe' pkgs.coreutils "mkdir"} -p ${cfg.dataDir}/data/blobs + '' + else + '' + ${lib.getExe' pkgs.coreutils "mkdir"} -p ${cfg.dataDir}/db + ''; + ExecStart = [ + "" + "${lib.getExe cfg.package} --config=${configFile}" + ]; + LoadCredential = lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials; + + ReadWritePaths = [ + cfg.dataDir + ]; + CacheDirectory = stalwartIdentifier; + StateDirectory = stalwartIdentifier; + + # Upstream uses "stalwart" as the username since 0.12.0 + User = cfg.user; + Group = cfg.group; + + # Bind standard privileged ports + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + + # Hardening + DeviceAllow = [ "" ]; + LockPersonality = true; + MemoryDenyWriteExecute = true; + PrivateDevices = true; + PrivateUsers = false; # incompatible with CAP_NET_BIND_SERVICE + ProcSubset = "pid"; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; + }; + unitConfig.ConditionPathExists = [ + "${configFile}" + ]; + }; + }; + + # Make admin commands available in the shell + environment.systemPackages = [ cfg.package ]; + + networking.firewall = + lib.mkIf (cfg.openFirewall && (builtins.hasAttr "listener" cfg.settings.server)) + { + allowedTCPPorts = parsePorts cfg.settings.server.listener; + }; + }; + + meta = { + maintainers = with lib.maintainers; [ + happysalada + onny + norpol + ]; + }; +} diff --git a/pkgs/common/stalwart/spam-filter.nix b/pkgs/common/stalwart/spam-filter.nix new file mode 100644 index 0000000..dd0ac51 --- /dev/null +++ b/pkgs/common/stalwart/spam-filter.nix @@ -0,0 +1,54 @@ +{ + lib, + fetchFromGitHub, + stdenvNoCC, + stalwart_0_16, + nix-update-script, + python3, +}: + +stdenvNoCC.mkDerivation (finalAttrs: { + pname = "spam-filter"; + version = "3.0.2"; + + src = fetchFromGitHub { + owner = "stalwartlabs"; + repo = "spam-filter"; + tag = "v${finalAttrs.version}"; + hash = "sha256-dMHfVzSTP/J+ohBIOIXZ3eKPVK+gnLZ0ifq5/u2EBFE="; + }; + + __structuredAttrs = true; + + nativeBuildInputs = [ python3 ]; + + buildPhase = '' + runHook preBuild + python3 generate_rules_json.py + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out + cp spam-filter-rules.json.gz $out + runHook postInstall + ''; + + passthru = { + updateScript = nix-update-script { }; + }; + + meta = { + description = "Spam filter module for the Stalwart server"; + homepage = "https://github.com/stalwartlabs/spam-filter"; + changelog = "https://github.com/stalwartlabs/spam-filter/blob/${finalAttrs.src.tag}/CHANGELOG.md"; + license = + with lib.licenses; + OR [ + mit + asl20 + ]; + inherit (stalwart_0_16.meta) maintainers; + }; +}) diff --git a/pkgs/common/stalwart/stalwart.nix b/pkgs/common/stalwart/stalwart.nix new file mode 100644 index 0000000..bfb8c24 --- /dev/null +++ b/pkgs/common/stalwart/stalwart.nix @@ -0,0 +1,315 @@ +{ + lib, + rustPlatform, + fetchFromGitHub, + pkg-config, + protobuf, + bzip2, + openssl, + sqlite, + foundationdb, + zstd, + stdenv, + _experimental-update-script-combinators, + nix-update-script, + rocksdb, + callPackage, + python3Packages, + cacert, + libredirect, + writeTextFile, + withFoundationdb ? false, + stalwartEnterprise ? false, +}: +let + migrate_v016 = + { + src, + version, + }: + python3Packages.buildPythonApplication { + pname = "migrate_v016"; + inherit src version; + __structuredAttrs = true; + format = "other"; + dontBuild = true; + dependencies = [ + python3Packages.urllib3 + python3Packages.requests + ]; + installPhase = '' + runHook preInstall + mkdir -p $out/bin + cp $src/resources/scripts/migrate_v016.py $out/bin/migrate_v016 + chmod +x $out/bin/migrate_v016 + runHook postInstall + ''; + postFixUp = '' + wrapPythonPrograms + ''; + }; +in +rustPlatform.buildRustPackage (finalAttrs: { + pname = "stalwart" + (lib.optionalString stalwartEnterprise "-enterprise"); + version = "0.16.26"; + + __structuredAttrs = true; + + src = fetchFromGitHub { + owner = "stalwartlabs"; + repo = "stalwart"; + tag = "v${finalAttrs.version}"; + hash = "sha256-CJ8XOWojIOD0/xBMDnZq0oS68vd77AEXkbT1nZx7bzY="; + }; + + cargoHash = "sha256-fE4+Dq6SGDA6FVDPO1Zf/Ow2VUALRQlPxGk+iETxqts="; + + env = { + # https://docs.rs/openssl/latest/openssl/#manual + OPENSSL_NO_VENDOR = true; + OPENSSL_DIR = lib.getDev openssl; + OPENSSL_LIB_DIR = "${lib.getLib openssl}/lib"; + ZSTD_SYS_USE_PKG_CONFIG = true; + ROCKSDB_INCLUDE_DIR = "${rocksdb}/include"; + ROCKSDB_LIB_DIR = "${rocksdb}/lib"; + } + // + lib.optionalAttrs + (stdenv.hostPlatform.isLinux && (stdenv.hostPlatform.isAarch64 || stdenv.hostPlatform.isArmv7)) + { + # Required to fix Stalwart on ARM systems with 16 KB page size. + # Using rust-jemalloc-sys causes segmentation faults on x86_64. + JEMALLOC_SYS_WITH_LG_PAGE = 16; + }; + + depsBuildBuild = [ + pkg-config + zstd + ]; + + nativeBuildInputs = [ + protobuf + rustPlatform.bindgenHook + ]; + + buildInputs = [ + bzip2 + openssl + sqlite + zstd + ] + ++ lib.optionals (stdenv.hostPlatform.isLinux && withFoundationdb) [ foundationdb ]; + + # Issue: https://github.com/stalwartlabs/stalwart/issues/1104 + buildNoDefaultFeatures = true; + buildFeatures = [ + "sqlite" + "postgres" + "mysql" + "rocks" + "s3" + "redis" + "azure" + "nats" + ] + ++ lib.optionals withFoundationdb [ "foundationdb" ] + ++ lib.optionals stalwartEnterprise [ "enterprise" ]; + + cargoBuildFlags = [ + "-p" + "stalwart" + ]; + cargoTestFlags = finalAttrs.cargoBuildFlags; + + doCheck = true; + nativeCheckInputs = [ + openssl + # ... panicked at crates/utils/src/lib.rs:94:17: Failed to build TLS connectors: Failed to build platform verifier: unexpected error: No CA certificates were loaded from the system + cacert + ]; + + # Allow network access during tests on Darwin/macOS + __darwinAllowLocalNetworking = true; + # Allow access to DNS settings exposed via config daemon + # thread ... panicked at tests/src/lib.rs:49:13: Errors: [ Build { ... , message: "Failed to read system DNS config: failed to access System Configuration dynamic store", }, ] + # -> https://github.com/hickory-dns/hickory-dns/blob/f09321075b1f97902b7bc4ca4ffda7816fcf2971/crates/resolver/src/system_conf/apple.rs#L21 + # -> https://crates.io/crates/system-configuration + # -> https://developer.apple.com/documentation/systemconfiguration/scdynamicstore?language=objc + # -> "The handle to an open dynamic store session with the system configuration daemon." (configd) + sandboxProfile = lib.optionalString stdenv.hostPlatform.isDarwin '' + (allow mach-lookup (global-name "com.apple.SystemConfiguration.configd")) + ''; + + preCheck = + let + nsswitch = writeTextFile { + name = "nsswitch.conf"; + text = '' + hosts: files dns + ''; + }; + hosts = writeTextFile { + name = "hosts"; + text = '' + 127.0.0.1 localhost + ''; + }; + # ... panicked at tests/src/lib.rs:49:13: Errors: [ Build { ... , message: "Failed to read system DNS config: io error: No such file or directory (os error 2)" }, ... ] + # -> https://github.com/hickory-dns/hickory-dns/blob/v0.26.1/crates/resolver/src/system_conf/unix.rs#L25 + # -> known issue: https://github.com/hickory-dns/hickory-dns/issues/2959 + resolvConf = writeTextFile { + name = "resolv.conf"; + text = '' + nameserver 127.0.0.1 + ''; + }; + in + (lib.optionalString stdenv.hostPlatform.isLinux '' + export NIX_REDIRECTS="/etc/nsswitch.conf=${nsswitch}:/etc/hosts=${hosts}:/etc/resolv.conf=${resolvConf}" + export LD_PRELOAD="${libredirect}/lib/libredirect.so" + '') + + '' + export STORE=RocksDb + ''; + + checkFlags = lib.forEach ( + [ + # Require running mysql, postgresql daemon + "directory::imap::imap_directory" + "directory::internal::internal_directory" + "directory::ldap::ldap_directory" + "directory::sql::sql_directory" + "directory::oidc::oidc_directory" + "store::blob::blob_tests" + "store::lookup::lookup_tests" + "smtp::lookup::sql::lookup_sql" + # Require running ldap directory + "directory::directory_tests" + # Require running nats or redis instance + "cluster::broadcast::cluster_tests" + # flaky tests: ... panicked at tests/src/utils/jmap.rs:434:32: Missing created item 0: { "methodResponses": [ [ "x:Domain/set", { "accountId": "d333333", "notCreated": { "i0": { "type": "primaryKeyViolation", "properties": [ "name" ], "objectId": { "object": "Domain", "id": "c" } } } }, "0" ] ], "sessionState": "a22d1fca" } + "cluster::stress::stress_tests" + "imap::imap_tests" + "system::system_tests" + "telemetry::telemetry_tests" + # flaky test: ... panicked at tests/src/utils/jmap.rs:434:32: Missing created item 0: { "methodResponses": [ [ "error", { "type": "serverUnavailable", description": "This server is temporarily unavailable. Attempting this same operation later may succeed." }, "0" ] ], "sessionState": "a22d1fca" } + "jmap::jmap_tests" + "automation::automation_tests" + # thread 'webdav::webdav_tests' (4676902) panicked at tests/src/utils/webdav.rs:591:13: Expected 201 Created but got 409 Conflict + "webdav::webdav_tests" + # panicked at tests/src/smtp/inbound/mod.rs:270:18: Unexpected event: WorkerDone { queue_id: 312710620013133824, queue_name: QueueName([100, 101, 102, 97, 117, 108, 116, 0]), status: Completed } + "smtp::outbound::smtp::smtp_delivery" + # flaky test: ... panicked at tests/src/smtp/outbound/lmtp.rs:245:5: assertion `left == right` failed + # left: 3 + # right: 4 + "smtp::outbound::lmtp::lmtp_delivery" + # flaky test: ... panicked at tests/src/smtp/reporting/analyze.rs:110:5: assertion `left == right` failed + # left: 0 + # right: 5 + "smtp::reporting::analyze::report_analyze" + # flaky test: ... panicked at tests/src/smtp/queue/manager.rs:200:32: Expected rcpt b not found in [Recipient { address: "c", retry: Schedule { due: 1781399468, inner: 0 }, notify: Schedule { due: 1781399473, inner: 0 }, expires: Ttl(9), queue: QueueName([100, 101, 102, 97, 117, 108, 116, 0]), status: Scheduled, flags: 0, orcpt: None }] + "smtp::queue::manager::queue_due" + # flaky test: ... panicked at tests/src/smtp/queue/retry.rs:237:5: assertion `left == right` failed + # left: [1, 2, 2] + # right: [1, 2, 3] + "smtp::queue::retry::queue_retry" + # error[E0432]: unresolved import `r2d2_sqlite` + "backend::sqlite::pool::SqliteConnectionManager::with_init" + # thread 'smtp::management::queue::manage_queue' panicked at tests/src/smtp/inbound/mod.rs:45:23: No queue event received. + # NOTE: Test unreliable on high load systems + "smtp::management::queue::manage_queue" + # NOTE: long running test(s), needs live dependencies, likely to fail on systems under high load + "smtp::inbound::antispam::antispam" + # flaky test (darwin): ... panicked at tests/src/smtp/inbound/mod.rs:287:18: + # Unexpected event: Refresh + # (linux): Invalid address: failed to lookup address information: Temporary failure in name resolution + "smtp::outbound::dane::dane_verify" + ] + # ... panicked at tests/src/lib.rs:49:13: Errors: [ Build { ... , message: "Invalid address: failed to lookup address information: Temporary failure in name resolution", }, ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + "smtp::inbound::auth::auth" + "smtp::inbound::basic::basic_commands" + "smtp::inbound::data::data" + "smtp::inbound::dmarc::dmarc" + "smtp::inbound::ehlo::ehlo" + "smtp::inbound::limits::limits" + "smtp::inbound::mail::mail" + "smtp::inbound::milter::milter_session" + "smtp::inbound::milter::mta_hook_session" + "smtp::inbound::rcpt::rcpt" + "smtp::inbound::rewrite::address_rewrite" + "smtp::inbound::scripts::sieve_scripts" + "smtp::inbound::sign::sign_and_seal" + "smtp::inbound::throttle::throttle_inbound" + "smtp::inbound::vrfy::vrfy_expn" + "smtp::lookup::expressions::expressions" + "smtp::lookup::utils::strategies" + "smtp::management::report::manage_reports" + "smtp::outbound::dane::dane_test" + "smtp::outbound::extensions::extensions" + "smtp::outbound::fallback_relay::fallback_relay" + "smtp::outbound::ip_lookup::ip_lookup_strategy" + "smtp::outbound::mta_sts::mta_sts_verify" + "smtp::outbound::throttle::throttle_outbound" + "smtp::outbound::tls::starttls_optional" + "smtp::queue::concurrent::concurrent_queue" + "smtp::queue::dsn::generate_dsn" + "smtp::queue::virtualq::virtual_queue" + "smtp::reporting::dmarc::report_dmarc" + "smtp::reporting::scheduler::report_scheduler" + "smtp::reporting::tls::report_tls" + "store::search_tests" + "store::store_tests" + # since 0.16.10 + "smtp::outbound::dane::dane_downgrade_on_tlsa_servfail" + ] + ) (test: "--skip=${test}"); + + postInstall = '' + mkdir -p $out/etc/stalwart + + mkdir -p $out/lib/systemd/system + + substitute resources/systemd/stalwart-mail.service $out/lib/systemd/system/stalwart.service \ + --replace-fail "__PATH__" "$out" + + ln -s ${migrate_v016 { inherit (finalAttrs) src version; }}/bin/migrate_v016 $out/bin/migrate_v016 + ''; + + passthru = { + inherit rocksdb; # make used rocksdb version available (e.g., for backup scripts) + webui = callPackage ./webui.nix { }; + spam-filter = callPackage ./spam-filter.nix { }; + # subpackages have distinct version numbers, so we can't use nix-update's `--subpackage` + updateScript = _experimental-update-script-combinators.sequence [ + (nix-update-script { }) + (nix-update-script { attrPath = "stalwart_0_16.webui"; }) + (nix-update-script { attrPath = "stalwart_0_16.spam-filter"; }) + ]; + }; + + meta = { + description = "Secure, modern, all-in-one mail and collaboration server"; + longDescription = '' + Secure, scalable and fluent in every protocol (IMAP, JMAP, SMTP, CalDAV, CardDAV, WebDAV). + ''; + homepage = "https://github.com/stalwartlabs/stalwart"; + changelog = "https://github.com/stalwartlabs/stalwart/blob/${finalAttrs.src.tag}/CHANGELOG.md"; + license = [ + lib.licenses.agpl3Only + ] + ++ lib.optionals stalwartEnterprise [ + lib.licenses.stalwart + ]; + maxSilent = 14400; # 4 hours + mainProgram = "stalwart"; + maintainers = with lib.maintainers; [ + happysalada + onny + oddlama + pandapip1 + norpol + debtquity + ]; + }; +}) diff --git a/pkgs/common/stalwart/webui.nix b/pkgs/common/stalwart/webui.nix new file mode 100644 index 0000000..1fe478d --- /dev/null +++ b/pkgs/common/stalwart/webui.nix @@ -0,0 +1,76 @@ +{ + lib, + buildNpmPackage, + zip, + stalwart_0_16, + fetchFromGitHub, + nix-update-script, +}: +buildNpmPackage (finalAttrs: { + pname = "webui"; + version = "1.0.11"; + + src = fetchFromGitHub { + owner = "stalwartlabs"; + repo = "webui"; + tag = "v${finalAttrs.version}"; + hash = "sha256-luaNHpPg7XHnPfQqhLEq+wW1/crXLogzyyltFr4mdBk="; + }; + + npmDepsHash = "sha256-qe9cSrvs6kWwgbOO0xL7MBaJvICOvyuLFVi9R0dgnXQ="; + __structuredAttrs = true; + + env = { + # https://github.com/stalwartlabs/webui/tree/main#environment-variables + # https://github.com/stalwartlabs/webui/blob/main/.env.development + VITE_API_BASE_URL = ""; + VITE_OAUTH_CLIENT_ID = "stalwart-webui"; + }; + + nativeBuildInputs = [ zip ]; + preBuild = '' + rm .env.development + ''; + + doCheck = true; + checkPhase = '' + runHook preCheck + npm run test + runHook postCheck + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out + cd dist + zip -r $out/webui.zip * + cd .. + runHook postInstall + ''; + + passthru = { + updateScript = nix-update-script { }; + }; + + meta = { + description = "Stalwart WebUI"; + longDescription = '' + Stalwart WebUI is schema-driven single-page application for administering Stalwart. + + After authentication the panel fetches a JSON schema from the server and dynamically + generates all forms, lists, navigation, and layouts from that schema. Nothing is hardcoded. + ''; + homepage = "https://github.com/stalwartlabs/webui"; + changelog = "https://github.com/stalwartlabs/webui/blob/${finalAttrs.src.tag}/CHANGELOG.md"; + license = lib.licenses.OR [ + lib.licenses.agpl3Only + { + fullName = "Stalwart Enterprise License 2.0 (SELv2) Agreement"; + url = "https://github.com/stalwartlabs/webui/blob/main/LICENSES/LicenseRef-SEL.txt"; + free = false; + redistributable = false; + } + ]; + inherit (stalwart_0_16.meta) maintainers; + }; +}) diff --git a/pkgs/default.nix b/pkgs/default.nix index 1c70741..d5f70dc 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -26,4 +26,5 @@ yubilock = pkgs.callPackage ./linux/yubilock/default.nix { }; ani-cli-rs = pkgs.callPackage ./linux/ani-cli-rs.nix { }; opengoal = pkgs.callPackage ./linux/opengoal.nix { }; + stalwart = pkgs.callPackage ./common/stalwart/stalwart.nix { }; }