1 { config, lib, pkgs, ... }:
3 cfg = config.services.listmonk;
4 tomlFormat = pkgs.formats.toml { };
5 cfgFile = tomlFormat.generate "listmonk.toml" cfg.settings;
6 # Escaping is done according to https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS
7 setDatabaseOption = key: value:
8 "UPDATE settings SET value = '${
9 lib.replaceStrings [ "'" ] [ "''" ] (builtins.toJSON value)
10 }' WHERE key = '${key}';";
11 updateDatabaseConfigSQL = pkgs.writeText "update-database-config.sql"
12 (lib.concatStringsSep "\n" (lib.mapAttrsToList setDatabaseOption
13 (if (cfg.database.settings != null) then
17 updateDatabaseConfigScript =
18 pkgs.writeShellScriptBin "update-database-config.sh" ''
19 ${if cfg.database.mutableSettings then ''
20 if [ ! -f /var/lib/listmonk/.db_settings_initialized ]; then
21 ${pkgs.postgresql}/bin/psql -d listmonk -f ${updateDatabaseConfigSQL} ;
22 touch /var/lib/listmonk/.db_settings_initialized
25 "${pkgs.postgresql}/bin/psql -d listmonk -f ${updateDatabaseConfigSQL}"}
28 databaseSettingsOpts = with lib.types; {
30 oneOf [ (listOf str) (listOf (attrsOf anything)) str int bool ];
33 "app.notify_emails" = lib.mkOption {
36 description = "Administrator emails for system notifications";
39 "privacy.exportable" = lib.mkOption {
41 default = [ "profile" "subscriptions" "campaign_views" "link_clicks" ];
43 "List of fields which can be exported through an automatic export request";
46 "privacy.domain_blocklist" = lib.mkOption {
50 "E-mail addresses with these domains are disallowed from subscribing.";
54 type = listOf (submodule {
55 freeformType = with lib.types; attrsOf anything;
58 enabled = lib.mkEnableOption "this SMTP server for listmonk";
61 description = "Hostname for the SMTP server";
64 type = lib.types.port;
65 description = "Port for the SMTP server";
67 max_conns = lib.mkOption {
70 "Maximum number of simultaneous connections, defaults to 1";
73 tls_type = lib.mkOption {
74 type = lib.types.enum [ "none" "STARTTLS" "TLS" ];
75 description = "Type of TLS authentication with the SMTP server";
80 description = "List of outgoing SMTP servers";
83 # TODO: refine this type based on the smtp one.
84 "bounce.mailboxes" = lib.mkOption {
86 (submodule { freeformType = with lib.types; listOf (attrsOf anything); });
88 description = "List of bounce mailboxes";
91 messengers = lib.mkOption {
95 "List of messengers, see: <https://github.com/knadh/listmonk/blob/master/models/settings.go#L64-L74> for options.";
102 services.listmonk = {
103 enable = lib.mkEnableOption "Listmonk, this module assumes a reverse proxy to be set";
105 createLocally = lib.mkOption {
106 type = lib.types.bool;
109 "Create the PostgreSQL database and database user locally.";
112 settings = lib.mkOption {
114 type = with lib.types; nullOr (submodule databaseSettingsOpts);
116 "Dynamic settings in the PostgreSQL database, set by a SQL script, see <https://github.com/knadh/listmonk/blob/master/schema.sql#L177-L230> for details.";
118 mutableSettings = lib.mkOption {
119 type = lib.types.bool;
122 Database settings will be reset to the value set in this module if this is not enabled.
123 Enable this if you want to persist changes you have done in the application.
127 package = lib.mkPackageOption pkgs "listmonk" {};
128 settings = lib.mkOption {
129 type = lib.types.submodule { freeformType = tomlFormat.type; };
131 Static settings set in the config.toml, see <https://github.com/knadh/listmonk/blob/master/config.toml.sample> for details.
132 You can set secrets using the secretFile option with environment variables following <https://listmonk.app/docs/configuration/#environment-variables>.
135 secretFile = lib.mkOption {
136 type = lib.types.nullOr lib.types.str;
139 "A file containing secrets as environment variables. See <https://listmonk.app/docs/configuration/#environment-variables> for details on supported values.";
144 ###### implementation
145 config = lib.mkIf cfg.enable {
146 # Default parameters from https://github.com/knadh/listmonk/blob/master/config.toml.sample
147 services.listmonk.settings."app".address = lib.mkDefault "localhost:9000";
148 services.listmonk.settings."db" = lib.mkMerge [
150 max_open = lib.mkDefault 25;
151 max_idle = lib.mkDefault 25;
152 max_lifetime = lib.mkDefault "300s";
154 (lib.mkIf cfg.database.createLocally {
155 host = lib.mkDefault "/run/postgresql";
156 port = lib.mkDefault 5432;
157 user = lib.mkDefault "listmonk";
158 database = lib.mkDefault "listmonk";
162 services.postgresql = lib.mkIf cfg.database.createLocally {
167 ensureDBOwnership = true;
170 ensureDatabases = [ "listmonk" ];
173 systemd.services.listmonk = {
174 description = "Listmonk - newsletter and mailing list manager";
175 after = [ "network.target" ]
176 ++ lib.optional cfg.database.createLocally "postgresql.service";
177 wantedBy = [ "multi-user.target" ];
180 EnvironmentFile = lib.mkIf (cfg.secretFile != null) [ cfg.secretFile ];
182 # StateDirectory cannot be used when DynamicUser = true is set this way.
183 # Indeed, it will try to create all the folders and realize one of them already exist.
184 # Therefore, we have to create it ourselves.
185 ''${pkgs.coreutils}/bin/mkdir -p "''${STATE_DIRECTORY}/listmonk/uploads"''
186 # setup database if not already done
187 "${cfg.package}/bin/listmonk --config ${cfgFile} --idempotent --install --yes"
188 # apply db migrations (setup and migrations can not be done in one step
189 # with "--install --upgrade" listmonk ignores the upgrade)
190 "${cfg.package}/bin/listmonk --config ${cfgFile} --upgrade --yes"
191 "${updateDatabaseConfigScript}/bin/update-database-config.sh"
193 ExecStart = "${cfg.package}/bin/listmonk --config ${cfgFile}";
195 Restart = "on-failure";
197 StateDirectory = [ "listmonk" ];
202 NoNewPrivileges = true;
203 CapabilityBoundingSet = "";
204 SystemCallArchitectures = "native";
205 SystemCallFilter = [ "@system-service" "~@privileged" ];
206 PrivateDevices = true;
207 ProtectControlGroups = true;
208 ProtectKernelTunables = true;
210 RestrictNamespaces = true;
211 RestrictRealtime = true;
213 MemoryDenyWriteExecute = true;
214 LockPersonality = true;
215 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
216 ProtectKernelModules = true;