summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakub Stachurski <jakub@wilkuu.xyz>2026-05-29 21:15:44 +0200
committerJakub Stachurski <jakub@wilkuu.xyz>2026-05-29 21:15:44 +0200
commit604b8e1850b19bf5acd2042b138dcd5a17698b65 (patch)
tree8a1e50c05999d8bf28f0f38146a9e25c60b55f92
parent2e300b08ee2697656a76b4c95a4c66039510a239 (diff)
Move stalwart 0.16 stuff into a separate flake
-rw-r--r--flake.lock24
-rw-r--r--flake.nix68
-rw-r--r--packages/stalwart-cli/package.nix52
-rw-r--r--packages/stalwart/package.nix211
-rw-r--r--packages/stalwart/spam-filter.nix42
-rw-r--r--packages/stalwart/webadmin.nix77
-rw-r--r--services/mail2.nix291
7 files changed, 351 insertions, 414 deletions
diff --git a/flake.lock b/flake.lock
index 81fb3de..10ad529 100644
--- a/flake.lock
+++ b/flake.lock
@@ -213,6 +213,7 @@
"nixpkgs": "nixpkgs",
"nixpkgs-stable": "nixpkgs-stable",
"sops-nix": "sops-nix",
+ "stalwart-nix": "stalwart-nix",
"treefmt-nix": "treefmt-nix"
}
},
@@ -253,6 +254,29 @@
"type": "github"
}
},
+ "stalwart-nix": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "treefmt-nix": [
+ "treefmt-nix"
+ ]
+ },
+ "locked": {
+ "lastModified": 1780081489,
+ "narHash": "sha256-ZALATER+KJLI0BumJtC7VhvpC3bYDTFvz/fMazoutGI=",
+ "owner": "Wilkuu-2",
+ "repo": "stalwart-nix",
+ "rev": "80bccc3e1ed1f5a99f13d4bc18402eb5eac99d76",
+ "type": "github"
+ },
+ "original": {
+ "owner": "Wilkuu-2",
+ "repo": "stalwart-nix",
+ "type": "github"
+ }
+ },
"treefmt-nix": {
"inputs": {
"nixpkgs": [
diff --git a/flake.nix b/flake.nix
index 72aa504..db1698c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -5,6 +5,14 @@
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-25.05";
+ stalwart-nix = {
+ # local testing
+ # url = "path:/store2/code/stalwart-nix";
+ url = "github:Wilkuu-2/stalwart-nix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ inputs.treefmt-nix.follows = "treefmt-nix";
+ };
+
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
@@ -42,46 +50,41 @@
nixpkgs,
treefmt-nix,
disko,
+ stalwart-nix,
+ sops-nix,
+ home-manager,
...
}@inputs:
let
lib = nixpkgs.lib;
systems = [
"x86_64-linux"
+ "x86_64-darwin"
"aarch64-linux"
+ "aarch64-darwin"
];
# Allows code to execute for all used architectures
- forAllSystems = f: (lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}));
+ pkgsPerSystem = (lib.genAttrs systems (system: nixpkgs.legacyPackages.${system}));
+ forAllSystems = f: (lib.genAttrs systems (system: f pkgsPerSystem.${system} system));
# Treefmt has a bunch of long paths that we want to bundle.
- treefmtStuff = forAllSystems (
- pkgs:
- let
- treefmt = treefmt-nix.lib.evalModule pkgs ./modules/treefmt.nix;
- in
- {
- formatter = treefmt.config.build.wrapper;
- formatCheck = {
- formatting = treefmt.config.build.check self;
- };
- }
- );
- # Convenient extractor which generates an attrset of system: attribute, with the attribute being picked from treefmtStuff by name.
- treefmtExtract = name: (builtins.mapAttrs (_system: conf: conf."${name}") (treefmtStuff));
+ treefmt = forAllSystems (pkgs: _: treefmt-nix.lib.evalModule pkgs ./modules/treefmt.nix);
in
{
- packages = let system = "x86_64-linux"; pkgs = import nixpkgs {inherit system;}; in {
- ${system} = {
- full-iso = self.nixosConfigurations.full-iso.config.system.build.isoImage;
- bulwark = pkgs.callPackage ./packages/bulwark/package.nix {};
- stalwart = pkgs.callPackage ./packages/stalwart/package.nix {};
- stalwart-cli = pkgs.callPackage ./packages/stalwart-cli/package.nix {};
+ packages =
+ forAllSystems (
+ pkgs: _system: {
+ bulwark = pkgs.callPackage ./packages/bulwark/package.nix { };
+ }
+ )
+ // {
+ "x86_64-linux".full-iso = self.nixosConfigurations.full-iso.config.system.build.isoImage;
};
- };
+
# for `nix fmt`
- formatter = treefmtExtract "formatter";
+ formatter = forAllSystems (_: system: treefmt.${system}.config.build.wrapper);
# for `nix flake check`
- checks = treefmtExtract "formatCheck";
+ checks = forAllSystems (_: system: { formatting = treefmt.${system}.config.build.check self; });
nixosConfigurations = {
apocalypse = nixpkgs.lib.nixosSystem {
@@ -93,8 +96,9 @@
./modules
./hosts/apocalypse
./users/wilkuu.nix
- inputs.home-manager.nixosModules.default
- inputs.sops-nix.nixosModules.sops
+ stalwart-nix.nixosModules.default
+ home-manager.nixosModules.default
+ sops-nix.nixosModules.default
];
};
full-iso = nixpkgs.lib.nixosSystem {
@@ -108,8 +112,8 @@
./modules
./hosts/full-iso
./users/live-user.nix
- inputs.home-manager.nixosModules.default
- inputs.sops-nix.nixosModules.sops
+ home-manager.nixosModules.default
+ sops-nix.nixosModules.default
];
};
omega-relay = nixpkgs.lib.nixosSystem {
@@ -121,9 +125,9 @@
./modules
./users/wilkuu-server.nix
./hosts/omega-relay
- inputs.home-manager.nixosModules.default
+ home-manager.nixosModules.default
disko.nixosModules.disko
- inputs.sops-nix.nixosModules.sops
+ sops-nix.nixosModules.default
];
};
@@ -136,9 +140,9 @@
./modules
./users/wilkuu-server.nix
./hosts/tacitus
- inputs.home-manager.nixosModules.default
+ home-manager.nixosModules.default
disko.nixosModules.disko
- inputs.sops-nix.nixosModules.sops
+ sops-nix.nixosModules.default
];
};
diff --git a/packages/stalwart-cli/package.nix b/packages/stalwart-cli/package.nix
deleted file mode 100644
index 6c89fa2..0000000
--- a/packages/stalwart-cli/package.nix
+++ /dev/null
@@ -1,52 +0,0 @@
-{
- lib,
- rustPlatform,
- versionCheckHook,
- stalwart,
- fetchFromGitHub,
- openssl,
- pkg-config,
- nix-update-script,
-}:
-rustPlatform.buildRustPackage (finalAttrs: {
- pname = "stalwart-cli";
- version = "1.0.7";
- src = fetchFromGitHub {
- owner = "stalwartlabs";
- repo = "cli";
- tag = "v${finalAttrs.version}";
- hash = "";
- };
-
- cargoHash = "";
-
- nativeBuildInputs = [ pkg-config ];
- buildInputs = [ openssl ];
-
- env.OPENSSL_NO_VENDOR = true;
-
- cargoBuildFlags = [
- "--package"
- "stalwart-cli"
- ];
- cargoTestFlags = [
- "--package"
- "stalwart-cli"
- ];
-
- doInstallCheck = true;
- nativeInstallCheckInputs = [ versionCheckHook ];
-
- passthru.updateScript = nix-update-script { };
-
- meta = {
- description = "Stalwart Mail Server CLI";
- mainProgram = "stalwart-cli";
- homepage = "https://github.com/stalwartlabs/cli";
- changelog = "https://github.com/stalwartlabs/cli/blob/main/CHANGELOG.md";
- license = lib.licenses.agpl3Only;
- # maintainers = with lib.maintainers; [
- # giomf
- # ];
- };
-})
diff --git a/packages/stalwart/package.nix b/packages/stalwart/package.nix
deleted file mode 100644
index 185fa5a..0000000
--- a/packages/stalwart/package.nix
+++ /dev/null
@@ -1,211 +0,0 @@
-{
- lib,
- rustPlatform,
- fetchFromGithub,
- pkg-config,
- protobuf,
- bzip2,
- openssl,
- sqlite,
- foundationdb,
- zstd,
- stdenv,
- nix-update-script,
- nixosTest,
- rocksdb,
- callPackage,
- withFoundationdb ? false,
- stalwartEnterprise ? false,
- buildPackages,
-}:
- rustPlatform.makeRustPackage (finalAttrs: {
- pname = "stalwart" + (lib.optionalString stalwartEnterprise "-enterprise");
- version = "0.16.6";
- src = fetchFromGithub {
- owner = "stalwartlabs";
- repo = "stalwart";
- tag = "v${finalAttrs.version}";
- hash = "";
- };
-
- cargoHash = "";
-
- depsBuildBuild = [
- pkg-config
- zstd
- ];
-
- nativeBuildInputs = [
- protobuf
- rustPlatform.bindgenHook
- ];
-
- buildInputs = [
- bzip2
- openssl
- sqlite
- zstd
- ] ++ lib.optional (stdenv.hostPlatform.isLinux && withFoundationdb) foundationdb;
-
- nativeCheckInputs = [
- openssl
- ];
-
- buildNoDefaultFeatures = true;
- buildFeatures = [
- "sqlite"
- "postgres"
- "mysql"
- "rocks"
- "s3"
- "redis"
- "azure"
- "nats"
- ]
- ++ lib.optionals withFoundationdb [ "foundationdb" ]
- ++ lib.optionals stalwartEnterprise [ "enterprise" ];
-
- env = {
- OPENSSL_NO_VENDOR = true;
- 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))
- {
- JEMALLOC_SYS_WITH_LG_PAGE = 16;
- };
-
- 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"
- '';
-
- preCheck = ''
- export STORE=Sqlite
- '';
- 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"
- # thread 'directory::smtp::lmtp_directory' panicked at tests/src/store/mod.rs:122:44:
- # called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
- "directory::smtp::lmtp_directory"
- # thread 'imap::imap_tests' panicked at tests/src/imap/mod.rs:436:14:
- # Missing store type. Try running `STORE=<store_type> cargo test`: NotPresent
- "imap::imap_tests"
- # thread 'jmap::jmap_tests' panicked at tests/src/jmap/mod.rs:303:14:
- # Missing store type. Try running `STORE=<store_type> cargo test`: NotPresent
- "jmap::jmap_tests"
- # Failed to read system DNS config: io error: No such file or directory (os error 2)
- "smtp::inbound::data::data"
- # Expected "X-My-Header: true" but got Received: from foobar.net (unknown [10.0.0.123])
- "smtp::inbound::scripts::sieve_scripts"
- # thread 'smtp::outbound::lmtp::lmtp_delivery' panicked at tests/src/smtp/session.rs:313:13:
- # Expected "<invalid@domain.org> (failed to lookup" but got From: "Mail Delivery Subsystem" <MAILER-DAEMON@localhost>
- "smtp::outbound::lmtp::lmtp_delivery"
- # thread 'smtp::outbound::extensions::extensions' panicked at tests/src/smtp/inbound/mod.rs:45:23:
- # No queue event received.
- "smtp::outbound::extensions::extensions"
- # panicked at tests/src/smtp/outbound/smtp.rs:173:5:
- "smtp::outbound::smtp::smtp_delivery"
- # panicked at tests/src/smtp/outbound/lmtp.rs
- "smtp::outbound::lmtp::lmtp_delivery"
- # thread 'smtp::queue::retry::queue_retry' panicked at tests/src/smtp/queue/retry.rs:119:5:
- # assertion `left == right` failed
- # left: [1, 2, 2]
- # right: [1, 2, 3]
- "smtp::queue::retry::queue_retry"
- # thread 'smtp::queue::virtualq::virtual_queue' panicked at /build/source/crates/store/src/dispatch/store.rs:548:14:
- # called `Result::unwrap()` on an `Err` value: Error(Event { inner: Store(MysqlError), keys: [(Reason, String("Input/output error: Input/output error: Connection refused (os error 111)")), (CausedBy, String("crates/store/src/dispatch/store.rs:301"))] })
- "smtp::queue::virtualq::virtual_queue"
- # Missing store type. Try running `STORE=<store_type> cargo test`: NotPresent
- "store::store_tests"
- # Missing store type. Try running `STORE=<store_type> cargo test`: NotPresent
- "cluster::cluster_tests"
- # Missing store type. Try running `STORE=<store_type> cargo test`: NotPresent
- "webdav::webdav_tests"
- # thread 'config::parser::tests::toml_parse' panicked at crates/utils/src/config/parser.rs:463:58:
- # called `Result::unwrap()` on an `Err` value: "Expected ['\\n'] but found '!' in value at line 70."
- "config::parser::tests::toml_parse"
- # error[E0432]: unresolved import `r2d2_sqlite`
- # use of undeclared crate or module `r2d2_sqlite`
- "backend::sqlite::pool::SqliteConnectionManager::with_init"
- # thread 'smtp::reporting::analyze::report_analyze' panicked at tests/src/smtp/reporting/analyze.rs:88:5:
- # assertion `left == right` failed
- # left: 0
- # right: 12
- "smtp::reporting::analyze::report_analyze"
- # thread 'smtp::inbound::dmarc::dmarc' panicked at tests/src/smtp/inbound/mod.rs:59:26:
- # Expected empty queue but got Reload
- "smtp::inbound::dmarc::dmarc"
- # thread 'smtp::queue::concurrent::concurrent_queue' panicked at tests/src/smtp/inbound/mod.rs:65:9:
- # assertion `left == right` failed
- "smtp::queue::concurrent::concurrent_queue"
- # Failed to read system DNS config: io error: No such file or directory (os error 2)
- "smtp::inbound::auth::auth"
- # Failed to read system DNS config: io error: No such file or directory (os error 2)
- "smtp::inbound::antispam::antispam"
- # Failed to read system DNS config: io error: No such file or directory (os error 2)
- "smtp::inbound::vrfy::vrfy_expn"
- # 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"
- # thread 'responses::tests::parse_responses' panicked at crates/dav-proto/src/responses/mod.rs:671:17:
- # assertion `left == right` failed: failed for 008.xml
- # left: ElementEnd
- # right: Bytes([...])
- "responses::tests::parse_responses"
- # thread 'store::search_tests' (912386) panicked at tests/src/store/mod.rs:116:10:
- # Missing store type. Try running `STORE=<store_type> cargo test`: NotPresent
- "store::search_tests"
- ] (test: "--skip=${test}");
-
- doCheck = !(stdenv.hostPlatform.isLinux && stdenv.hostPlatform.isAarch64);
-
- __darwinAllowLocalNetworking = true;
-
- passthru = {
- inherit rocksdb;
- # webadmin = buildPackages.callPackage ./webadmin.nix {};
- # spam-filter = callPackage ./spam-filter.nix {};
- updateScript = nix-update-script { };
- # test.stalwart = nixosTests.stalwart;
- };
-
- 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/main/CHANGELOG.md";
- license = [
- lib.licenses.agpl3Only
- ]
- ++ lib.optionals stalwartEnterprise [
- {
- fullName = "Stalwart Enterprise License 1.0 (SELv1) Agreement";
- url = "https://github.com/stalwartlabs/stalwart/blob/main/LICENSES/LicenseRef-SEL.txt";
- free = false;
- redistributable = false;
- }
- ];
-
- mainProgram = "stalwart";
- };
-})
-
diff --git a/packages/stalwart/spam-filter.nix b/packages/stalwart/spam-filter.nix
deleted file mode 100644
index 5497322..0000000
--- a/packages/stalwart/spam-filter.nix
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- lib,
- fetchFromGithub,
- stdenv,
- stalwart,
- nix-update-script,
-}: stdenv.mkDerivation (finalAttrs: {
- pname = "spam-filter";
- version = "2.0.5";
-
- src = fetchFromGithub {
- owner = "stalwartlabs";
- repo = "spam-filter";
- tag = "v${finalAttrs.version}";
- hash = "";
- };
-
- buildPhase = ''
- bash ./build.sh
- '';
-
- installPhase = ''
- mkdir -p $out
- cp spam-filter.toml $out/
- '';
-
- 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; [
- mit
- asl20
- ];
- # inherit (stalwart.meta) maintainers;
- };
-
-
-})
-
diff --git a/packages/stalwart/webadmin.nix b/packages/stalwart/webadmin.nix
deleted file mode 100644
index 0035bda..0000000
--- a/packages/stalwart/webadmin.nix
+++ /dev/null
@@ -1,77 +0,0 @@
-{
- lib,
- rustPlatform,
- stalwart,
- fetchFromGitHub,
- trunk,
- tailwindcss_3,
- fetchNpmDeps,
- nix-update-script,
- nodejs,
- npmHooks,
- llvmPackages,
- wasm-bindgen-cli_0_2_93,
- binaryen,
- zip,
-}:
-
-rustPlatform.buildRustPackage (finalAttrs: {
- pname = "webadmin";
- version = "0.1.37";
-
- src = fetchFromGitHub {
- owner = "stalwartlabs";
- repo = "webadmin";
- tag = "v${finalAttrs.version}";
- hash = "";
- };
-
- npmDeps = fetchNpmDeps {
- name = "${finalAttrs.pname}-npm-deps";
- hash = "";
- };
-
- cargoHash = "";
-
- postPatch = ''
- # Using local tailwindcss for compilation
- substituteInPlace Trunk.toml --replace-fail "npx tailwindcss" "tailwindcss"
- '';
-
- nativeBuildInputs = [
- binaryen
- llvmPackages.bintools-unwrapped
- nodejs
- npmHooks.npmConfigHook
- tailwindcss_3
- trunk
- # needs to match with wasm-bindgen version in upstreams Cargo.lock
- wasm-bindgen-cli_0_2_93
-
- zip
- ];
-
- env.NODE_PATH = "$npmDeps";
-
- buildPhase = ''
- trunk build --offline --frozen --release
- '';
-
- installPhase = ''
- cd dist
- mkdir -p $out
- zip -r $out/webadmin.zip *
- '';
-
- passthru = {
- updateScript = nix-update-script { };
- };
-
- meta = {
- description = "Web administration module for the Stalwart server";
- homepage = "https://github.com/stalwartlabs/webadmin";
- changelog = "https://github.com/stalwartlabs/webadmin/blob/${finalAttrs.src.tag}/CHANGELOG.md";
- license = lib.licenses.agpl3Only;
- # inherit (stalwart.meta) maintainers;
- };
-})
diff --git a/services/mail2.nix b/services/mail2.nix
new file mode 100644
index 0000000..dcea8c0
--- /dev/null
+++ b/services/mail2.nix
@@ -0,0 +1,291 @@
+{
+ config,
+ lib,
+ ...
+}:
+let
+ cfg = config.wilkuu.services.mail;
+ hostname = config.networking.hostName;
+in
+{
+ imports = [ ./stalwart0_16.nix ];
+ options.wilkuu.services.mail = with lib; {
+ enable = mkEnableOption "Enable webmail";
+ doACME = mkEnableOption "Enable ACME for stalwart here";
+ defaultDomain = mkOption {
+ type = lib.types.str;
+ default = "mail.${hostname}.local";
+ example = "mail.wilkuu.xyz";
+ description = "Domain for http connections.";
+ };
+ domains = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ example = [ "mail.wilkuu.xyz" ];
+ description = "Domains for email.";
+ };
+ wellKnownDomains = mkOption {
+ type = types.listOf types.str;
+ default = [ "${hostname}.local" ];
+ example = [ "wilkuu.xyz" ];
+ description = "Domain for well-known items";
+ };
+ extraConfig = mkOption {
+ type = types.listOf types.attrs;
+ description = "Additional plan steps added to the stalwart config";
+ default = [ ];
+ example = [ ];
+ };
+ extraCreate = mkOption {
+ type = types.listOf types.attrs;
+ description = "Additional idempotent create steps added to the stalwart config";
+ default = [ ];
+ example = [ ];
+ };
+
+ startupMode = mkOption {
+ type = types.enum [
+ "normal"
+ "bootstrap"
+ "recovery"
+ ];
+ description = "Whenever to use the bootstrap or recovery mode, see https://stalw.art/docs/configuration/bootstrap-mode/ and https://stalw.art/docs/configuration/recovery-mode/";
+ default = "normal";
+ example = "bootstrap";
+ };
+
+ };
+
+ config = lib.mkIf cfg.enable (
+ let
+ tools = config.wilkuu.services.stalwart16.toolbox;
+ sopsPath = ../secrets/${hostname}/stalwart.yaml;
+ secrets = [
+ "admin_user"
+ "admin_password"
+ "recovery_user"
+ "recovery_password"
+ ];
+ toSops = (sname: "stalwart16/${sname}");
+ toPlaceholder = (sname: config.sops.placeholder.${toSops sname});
+ toCredfilePath = (name: config.sops.secrets.${toSops name}.path);
+
+ domain_to_jid = lib.replaceString "." "_";
+ # We do this to satisfy the foreign key constraint of the SystemSettings singleton
+ placeholderDomain = "bootstrap-placeholder.home.arpa";
+ planPreamble = lib.concatLists [
+ (tools.mkIdempotentCreateLine {
+ "object" = "Domain";
+ deleteBy = "name";
+ value."#placeholder-domain" = {
+ name = placeholderDomain;
+ certificateManagement = {
+ "@type" = "Manual";
+ };
+ dnsManagement = {
+ "@type" = "Manual";
+ };
+ dkimManagement = {
+ "@type" = "Manual";
+ };
+ subAddressing = {
+ "@type" = "Enabled";
+ };
+ };
+ })
+ [
+ {
+ "@type" = "update";
+ "object" = "SystemSettings";
+ "value" = {
+ "defaultDomainId" = "#placeholder-domain";
+ };
+ }
+ ]
+ ];
+ # TODO: Is this nice, or is using 1 object better?
+ # This approach makes it more atomic afaik?
+ domainCreateRules = (
+ lib.forEach cfg.domains (domain: {
+ object = "Domain";
+ deleteBy = "name";
+ value.${domain_to_jid domain} = {
+ name = domain;
+ certificateManagement = {
+ "@type" = "Manual";
+ };
+ dnsManagement = {
+ "@type" = "Manual";
+ };
+ dkimManagement = {
+ "@type" = "Manual";
+ };
+ subAddressing = {
+ "@type" = "Enabled";
+ };
+ };
+ })
+ );
+ certificateCreateRules = lib.optionals cfg.doACME (
+ lib.forEach (lib.unique ([ cfg.default_domain ] ++ cfg.domains)) (
+ (domain: {
+ object = "Certificate";
+ deleteBy = "certificate.filePath";
+ value."cert_${domain_to_jid domain}" = {
+ certificate = {
+ "@type" = "File";
+ filePath = "/run/credentials/stalwart.service/tls_${domain}_cert.pem";
+ };
+ privateKey = {
+ "@type" = "File";
+ filePath = "/run/credentials/stalwart.service/tls_${domain}_key.pem";
+ };
+ };
+ })
+
+ )
+ );
+ baseSetupRules = [
+ {
+ "@type" = "update";
+ "object" = "SystemSettings";
+ "value" = {
+ "defaultDomainId" = "#${domain_to_jid (builtins.elemAt cfg.domains 0)}";
+ "defaultHostname" = cfg.defaultDomain;
+ };
+ }
+ {
+ "@type" = "update";
+ "object" = "BlobStore";
+ "value" = {
+ "@type" = "Default";
+ };
+ }
+ {
+ "@type" = "update";
+ "object" = "InMemoryStore";
+ "value" = {
+ "@type" = "Default";
+ };
+ }
+ {
+ "@type" = "update";
+ "object" = "SearchStore";
+ "value" = {
+ "@type" = "Default";
+ };
+ }
+ ];
+ proxyWellKnown =
+ names:
+ let
+ uris = map (n: "/.well-known/${n}") names;
+ in
+ (lib.genAttrs uris (uri: {
+ proxyPass = "http://localhost:3080${uri}";
+ recommendedProxySettings = true;
+ }));
+ makeHTTPRedirectBody = target: https: "302 ${if https then "https" else "http"}://${target}";
+
+ in
+ {
+ users.users.stalwart = {
+ isSystemUser = true;
+ group = "stalwart";
+ };
+ users.groups.stalwart = { };
+ sops.secrets = (
+ lib.genAttrs (map toSops secrets) (_name: {
+ sopsFile = sopsPath;
+ mode = "0440";
+ owner = "stalwart";
+ })
+ );
+
+ sops.templates = {
+ stalwart-config-creds = {
+ owner = "stalwart";
+ mode = "0440";
+ content = ''
+ STALWART_USER=${toPlaceholder "admin_user"}
+ STALWART_PASSWORD=${toPlaceholder "admin_password"}
+ '';
+ };
+ stalwart-recovery-creds = {
+ owner = "stalwart";
+ mode = "0440";
+ content = ''
+ STALWART_RECOVERY_ADMIN=${toPlaceholder "recovery_user"}:${toPlaceholder "recovery_password"}
+ '';
+ };
+ };
+
+ services.nginx.enable = lib.mkDefault true;
+ services.nginx.virtualHosts =
+ (lib.genAttrs
+ (lib.concatLists [
+ cfg.wellKnownDomains
+ cfg.domains
+ [ cfg.defaultDomain ]
+ ])
+ (_wdomain: {
+ addSSL = lib.mkDefault cfg.doACME;
+ enableACME = lib.mkDefault cfg.doACME;
+ locations =
+ (proxyWellKnown [
+ "mta-sts.txt"
+ "mail-v1.xml"
+ "autoconfig/mail"
+ "openid-configuration"
+ "/.well-known/oauth-authorization-server"
+ ])
+ // (lib.genAttrs [ "/.well-known/caldav/" "/.well-known/webdav/" "/.well-known/jmap" ] (uri: {
+ extraConfig = ''
+ return ${makeHTTPRedirectBody "${cfg.domain}${uri}" cfg.doACME};
+ '';
+ }));
+
+ })
+ )
+ // (lib.genAttrs [ cfg.defaultDomain ] (_domain: {
+ addSSL = cfg.doACME;
+ enableACME = cfg.doACME;
+ #serverName = "${domain}";
+ locations."/" = {
+ proxyPass = "http://localhost:8080";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ };
+ }));
+
+ wilkuu.services.stalwart16 = {
+ enable = cfg.enable;
+ url = if cfg.startupMode != "normal" then "http://localhost:8080/" else cfg.defaultDomain;
+ credentialsFile = config.sops.templates.stalwart-config-creds.path;
+ recoveryCredentialsFile = config.sops.templates.stalwart-recovery-creds.path;
+ startupMode = cfg.startupMode;
+ user = "stalwart";
+ group = "stalwart";
+ configPlanPre = planPreamble;
+ idempotentCreate = certificateCreateRules ++ domainCreateRules ++ cfg.extraCreate;
+ configPlanPost = baseSetupRules ++ cfg.extraConfig;
+ credentials =
+ (lib.genAttrs secrets toCredfilePath)
+ // (builtins.foldl' (a: b: a // b) ({ }) (
+ map (
+ domain:
+ let
+ acme_dir = config.security.acme.certs.${domain}.directory;
+ cert_path = file: "${acme_dir}/${file}";
+ in
+ {
+ "tls_${domain}_cert.pem" = cert_path "cert.pem";
+ "tls_${domain}_key.pem" = cert_path "key.pem";
+ }
+ ) (lib.optionals cfg.doACME (lib.unique ([ cfg.defaultDomain ] ++ cfg.domains)))
+ ));
+ };
+ }
+ );
+
+}