add: stalwart package, nixos module

ripped from nixpkgs, maintaining stalwart myself for a bit
This commit is contained in:
wymiller committed 2026-10-21 19:17:30 -04:00
1 parent 11a3e35ec6
commit a86df3a7ab
6 files changed
+760

No files matched your search

+311
View File
@@ -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 <https://stalw.art/docs/configuration> 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
];
};
}