at 24.11-pre 8.1 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4let 5 cfg = config.services.listmonk; 6 tomlFormat = pkgs.formats.toml { }; 7 cfgFile = tomlFormat.generate "listmonk.toml" cfg.settings; 8 # Escaping is done according to https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS 9 setDatabaseOption = key: value: 10 "UPDATE settings SET value = '${ 11 lib.replaceStrings [ "'" ] [ "''" ] (builtins.toJSON value) 12 }' WHERE key = '${key}';"; 13 updateDatabaseConfigSQL = pkgs.writeText "update-database-config.sql" 14 (concatStringsSep "\n" (mapAttrsToList setDatabaseOption 15 (if (cfg.database.settings != null) then 16 cfg.database.settings 17 else 18 { }))); 19 updateDatabaseConfigScript = 20 pkgs.writeShellScriptBin "update-database-config.sh" '' 21 ${if cfg.database.mutableSettings then '' 22 if [ ! -f /var/lib/listmonk/.db_settings_initialized ]; then 23 ${pkgs.postgresql}/bin/psql -d listmonk -f ${updateDatabaseConfigSQL} ; 24 touch /var/lib/listmonk/.db_settings_initialized 25 fi 26 '' else 27 "${pkgs.postgresql}/bin/psql -d listmonk -f ${updateDatabaseConfigSQL}"} 28 ''; 29 30 databaseSettingsOpts = with types; { 31 freeformType = 32 oneOf [ (listOf str) (listOf (attrsOf anything)) str int bool ]; 33 34 options = { 35 "app.notify_emails" = mkOption { 36 type = listOf str; 37 default = [ ]; 38 description = "Administrator emails for system notifications"; 39 }; 40 41 "privacy.exportable" = mkOption { 42 type = listOf str; 43 default = [ "profile" "subscriptions" "campaign_views" "link_clicks" ]; 44 description = 45 "List of fields which can be exported through an automatic export request"; 46 }; 47 48 "privacy.domain_blocklist" = mkOption { 49 type = listOf str; 50 default = [ ]; 51 description = 52 "E-mail addresses with these domains are disallowed from subscribing."; 53 }; 54 55 smtp = mkOption { 56 type = listOf (submodule { 57 freeformType = with types; attrsOf anything; 58 59 options = { 60 enabled = mkEnableOption "this SMTP server for listmonk"; 61 host = mkOption { 62 type = types.str; 63 description = "Hostname for the SMTP server"; 64 }; 65 port = mkOption { 66 type = types.port; 67 description = "Port for the SMTP server"; 68 }; 69 max_conns = mkOption { 70 type = types.int; 71 description = 72 "Maximum number of simultaneous connections, defaults to 1"; 73 default = 1; 74 }; 75 tls_type = mkOption { 76 type = types.enum [ "none" "STARTTLS" "TLS" ]; 77 description = "Type of TLS authentication with the SMTP server"; 78 }; 79 }; 80 }); 81 82 description = "List of outgoing SMTP servers"; 83 }; 84 85 # TODO: refine this type based on the smtp one. 86 "bounce.mailboxes" = mkOption { 87 type = listOf 88 (submodule { freeformType = with types; listOf (attrsOf anything); }); 89 default = [ ]; 90 description = "List of bounce mailboxes"; 91 }; 92 93 messengers = mkOption { 94 type = listOf str; 95 default = [ ]; 96 description = 97 "List of messengers, see: <https://github.com/knadh/listmonk/blob/master/models/settings.go#L64-L74> for options."; 98 }; 99 }; 100 }; 101in { 102 ###### interface 103 options = { 104 services.listmonk = { 105 enable = mkEnableOption "Listmonk, this module assumes a reverse proxy to be set"; 106 database = { 107 createLocally = mkOption { 108 type = types.bool; 109 default = false; 110 description = 111 "Create the PostgreSQL database and database user locally."; 112 }; 113 114 settings = mkOption { 115 default = null; 116 type = with types; nullOr (submodule databaseSettingsOpts); 117 description = 118 "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."; 119 }; 120 mutableSettings = mkOption { 121 type = types.bool; 122 default = true; 123 description = '' 124 Database settings will be reset to the value set in this module if this is not enabled. 125 Enable this if you want to persist changes you have done in the application. 126 ''; 127 }; 128 }; 129 package = mkPackageOption pkgs "listmonk" {}; 130 settings = mkOption { 131 type = types.submodule { freeformType = tomlFormat.type; }; 132 description = '' 133 Static settings set in the config.toml, see <https://github.com/knadh/listmonk/blob/master/config.toml.sample> for details. 134 You can set secrets using the secretFile option with environment variables following <https://listmonk.app/docs/configuration/#environment-variables>. 135 ''; 136 }; 137 secretFile = mkOption { 138 type = types.nullOr types.str; 139 default = null; 140 description = 141 "A file containing secrets as environment variables. See <https://listmonk.app/docs/configuration/#environment-variables> for details on supported values."; 142 }; 143 }; 144 }; 145 146 ###### implementation 147 config = mkIf cfg.enable { 148 # Default parameters from https://github.com/knadh/listmonk/blob/master/config.toml.sample 149 services.listmonk.settings."app".address = mkDefault "localhost:9000"; 150 services.listmonk.settings."db" = mkMerge [ 151 ({ 152 max_open = mkDefault 25; 153 max_idle = mkDefault 25; 154 max_lifetime = mkDefault "300s"; 155 }) 156 (mkIf cfg.database.createLocally { 157 host = mkDefault "/run/postgresql"; 158 port = mkDefault 5432; 159 user = mkDefault "listmonk"; 160 database = mkDefault "listmonk"; 161 }) 162 ]; 163 164 services.postgresql = mkIf cfg.database.createLocally { 165 enable = true; 166 167 ensureUsers = [{ 168 name = "listmonk"; 169 ensureDBOwnership = true; 170 }]; 171 172 ensureDatabases = [ "listmonk" ]; 173 }; 174 175 systemd.services.listmonk = { 176 description = "Listmonk - newsletter and mailing list manager"; 177 after = [ "network.target" ] 178 ++ optional cfg.database.createLocally "postgresql.service"; 179 wantedBy = [ "multi-user.target" ]; 180 serviceConfig = { 181 Type = "exec"; 182 EnvironmentFile = mkIf (cfg.secretFile != null) [ cfg.secretFile ]; 183 ExecStartPre = [ 184 # StateDirectory cannot be used when DynamicUser = true is set this way. 185 # Indeed, it will try to create all the folders and realize one of them already exist. 186 # Therefore, we have to create it ourselves. 187 ''${pkgs.coreutils}/bin/mkdir -p "''${STATE_DIRECTORY}/listmonk/uploads"'' 188 # setup database if not already done 189 "${cfg.package}/bin/listmonk --config ${cfgFile} --idempotent --install --yes" 190 # apply db migrations (setup and migrations can not be done in one step 191 # with "--install --upgrade" listmonk ignores the upgrade) 192 "${cfg.package}/bin/listmonk --config ${cfgFile} --upgrade --yes" 193 "${updateDatabaseConfigScript}/bin/update-database-config.sh" 194 ]; 195 ExecStart = "${cfg.package}/bin/listmonk --config ${cfgFile}"; 196 197 Restart = "on-failure"; 198 199 StateDirectory = [ "listmonk" ]; 200 201 User = "listmonk"; 202 Group = "listmonk"; 203 DynamicUser = true; 204 NoNewPrivileges = true; 205 CapabilityBoundingSet = ""; 206 SystemCallArchitectures = "native"; 207 SystemCallFilter = [ "@system-service" "~@privileged" ]; 208 PrivateDevices = true; 209 ProtectControlGroups = true; 210 ProtectKernelTunables = true; 211 ProtectHome = true; 212 RestrictNamespaces = true; 213 RestrictRealtime = true; 214 UMask = "0027"; 215 MemoryDenyWriteExecute = true; 216 LockPersonality = true; 217 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; 218 ProtectKernelModules = true; 219 PrivateUsers = true; 220 }; 221 }; 222 }; 223}