grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / mail / listmonk.nix
blob82c94ad4bb8f35204c9ab03d04ed65c34bbc6f9e
1 { config, lib, pkgs, ... }:
2 let
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
14         cfg.database.settings
15       else
16         { })));
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
23         fi
24       '' else
25         "${pkgs.postgresql}/bin/psql -d listmonk -f ${updateDatabaseConfigSQL}"}
26     '';
28   databaseSettingsOpts = with lib.types; {
29     freeformType =
30       oneOf [ (listOf str) (listOf (attrsOf anything)) str int bool ];
32     options = {
33       "app.notify_emails" = lib.mkOption {
34         type = listOf str;
35         default = [ ];
36         description = "Administrator emails for system notifications";
37       };
39       "privacy.exportable" = lib.mkOption {
40         type = listOf str;
41         default = [ "profile" "subscriptions" "campaign_views" "link_clicks" ];
42         description =
43           "List of fields which can be exported through an automatic export request";
44       };
46       "privacy.domain_blocklist" = lib.mkOption {
47         type = listOf str;
48         default = [ ];
49         description =
50           "E-mail addresses with these domains are disallowed from subscribing.";
51       };
53       smtp = lib.mkOption {
54         type = listOf (submodule {
55           freeformType = with lib.types; attrsOf anything;
57           options = {
58             enabled = lib.mkEnableOption "this SMTP server for listmonk";
59             host = lib.mkOption {
60               type = lib.types.str;
61               description = "Hostname for the SMTP server";
62             };
63             port = lib.mkOption {
64               type = lib.types.port;
65               description = "Port for the SMTP server";
66             };
67             max_conns = lib.mkOption {
68               type = lib.types.int;
69               description =
70                 "Maximum number of simultaneous connections, defaults to 1";
71               default = 1;
72             };
73             tls_type = lib.mkOption {
74               type = lib.types.enum [ "none" "STARTTLS" "TLS" ];
75               description = "Type of TLS authentication with the SMTP server";
76             };
77           };
78         });
80         description = "List of outgoing SMTP servers";
81       };
83       # TODO: refine this type based on the smtp one.
84       "bounce.mailboxes" = lib.mkOption {
85         type = listOf
86           (submodule { freeformType = with lib.types; listOf (attrsOf anything); });
87         default = [ ];
88         description = "List of bounce mailboxes";
89       };
91       messengers = lib.mkOption {
92         type = listOf str;
93         default = [ ];
94         description =
95           "List of messengers, see: <https://github.com/knadh/listmonk/blob/master/models/settings.go#L64-L74> for options.";
96       };
97     };
98   };
99 in {
100   ###### interface
101   options = {
102     services.listmonk = {
103       enable = lib.mkEnableOption "Listmonk, this module assumes a reverse proxy to be set";
104       database = {
105         createLocally = lib.mkOption {
106           type = lib.types.bool;
107           default = false;
108           description =
109             "Create the PostgreSQL database and database user locally.";
110         };
112         settings = lib.mkOption {
113           default = null;
114           type = with lib.types; nullOr (submodule databaseSettingsOpts);
115           description =
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.";
117         };
118         mutableSettings = lib.mkOption {
119           type = lib.types.bool;
120           default = true;
121           description = ''
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.
124           '';
125         };
126       };
127       package = lib.mkPackageOption pkgs "listmonk" {};
128       settings = lib.mkOption {
129         type = lib.types.submodule { freeformType = tomlFormat.type; };
130         description = ''
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>.
133         '';
134       };
135       secretFile = lib.mkOption {
136         type = lib.types.nullOr lib.types.str;
137         default = null;
138         description =
139           "A file containing secrets as environment variables. See <https://listmonk.app/docs/configuration/#environment-variables> for details on supported values.";
140       };
141     };
142   };
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 [
149       ({
150         max_open = lib.mkDefault 25;
151         max_idle = lib.mkDefault 25;
152         max_lifetime = lib.mkDefault "300s";
153       })
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";
159       })
160     ];
162     services.postgresql = lib.mkIf cfg.database.createLocally {
163       enable = true;
165       ensureUsers = [{
166         name = "listmonk";
167         ensureDBOwnership = true;
168       }];
170       ensureDatabases = [ "listmonk" ];
171     };
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" ];
178       serviceConfig = {
179         Type = "exec";
180         EnvironmentFile = lib.mkIf (cfg.secretFile != null) [ cfg.secretFile ];
181         ExecStartPre = [
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"
192         ];
193         ExecStart = "${cfg.package}/bin/listmonk --config ${cfgFile}";
195         Restart = "on-failure";
197         StateDirectory = [ "listmonk" ];
199         User = "listmonk";
200         Group = "listmonk";
201         DynamicUser = true;
202         NoNewPrivileges = true;
203         CapabilityBoundingSet = "";
204         SystemCallArchitectures = "native";
205         SystemCallFilter = [ "@system-service" "~@privileged" ];
206         PrivateDevices = true;
207         ProtectControlGroups = true;
208         ProtectKernelTunables = true;
209         ProtectHome = true;
210         RestrictNamespaces = true;
211         RestrictRealtime = true;
212         UMask = "0027";
213         MemoryDenyWriteExecute = true;
214         LockPersonality = true;
215         RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
216         ProtectKernelModules = true;
217         PrivateUsers = true;
218       };
219     };
220   };