about summary refs log tree commit diff
path: root/nixos/modules/services/mail/listmonk.nix
diff options
context:
space:
mode:
authorRaito Bezarius2022-08-30 13:15:41 +0200
committerRaito Bezarius2022-09-21 19:55:20 +0200
commit6b891f4788a302f9e847e808b32780066b268864 (patch)
tree12c35388929ccc686c7ac8973445526c1e393e2d /nixos/modules/services/mail/listmonk.nix
parentf3b7d6414b06eb493ed7f258a6938be440b7cdaf (diff)
nixos/listmonk: init module
Diffstat (limited to 'nixos/modules/services/mail/listmonk.nix')
-rw-r--r--nixos/modules/services/mail/listmonk.nix222
1 files changed, 222 insertions, 0 deletions
diff --git a/nixos/modules/services/mail/listmonk.nix b/nixos/modules/services/mail/listmonk.nix
new file mode 100644
index 000000000000..7c298606a547
--- /dev/null
+++ b/nixos/modules/services/mail/listmonk.nix
@@ -0,0 +1,222 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.listmonk;
+  tomlFormat = pkgs.formats.toml { };
+  cfgFile = tomlFormat.generate "listmonk.toml" cfg.settings;
+  # Escaping is done according to https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS
+  setDatabaseOption = key: value:
+    "UPDATE settings SET value = '${
+      lib.replaceChars [ "'" ] [ "''" ] (builtins.toJSON value)
+    }' WHERE key = '${key}';";
+  updateDatabaseConfigSQL = pkgs.writeText "update-database-config.sql"
+    (concatStringsSep "\n" (mapAttrsToList setDatabaseOption
+      (if (cfg.database.settings != null) then
+        cfg.database.settings
+      else
+        { })));
+  updateDatabaseConfigScript =
+    pkgs.writeShellScriptBin "update-database-config.sh" ''
+      ${if cfg.database.mutableSettings then ''
+        if [ ! -f /var/lib/listmonk/.db_settings_initialized ]; then
+          ${pkgs.postgresql}/bin/psql -d listmonk -f ${updateDatabaseConfigSQL} ;
+          touch /var/lib/listmonk/.db_settings_initialized
+        fi
+      '' else
+        "${pkgs.postgresql}/bin/psql -d listmonk -f ${updateDatabaseConfigSQL}"}
+    '';
+
+  databaseSettingsOpts = with types; {
+    freeformType =
+      oneOf [ (listOf str) (listOf (attrsOf anything)) str int bool ];
+
+    options = {
+      "app.notify_emails" = mkOption {
+        type = listOf str;
+        default = [ ];
+        description = lib.mdDoc "Administrator emails for system notifications";
+      };
+
+      "privacy.exportable" = mkOption {
+        type = listOf str;
+        default = [ "profile" "subscriptions" "campaign_views" "link_clicks" ];
+        description = lib.mdDoc
+          "List of fields which can be exported through an automatic export request";
+      };
+
+      "privacy.domain_blocklist" = mkOption {
+        type = listOf str;
+        default = [ ];
+        description = lib.mdDoc
+          "E-mail addresses with these domains are disallowed from subscribing.";
+      };
+
+      smtp = mkOption {
+        type = listOf (submodule {
+          freeformType = with types; attrsOf (oneOf [ str int bool ]);
+
+          options = {
+            enabled = mkEnableOption (lib.mdDoc "this SMTP server for listmonk");
+            host = mkOption {
+              type = types.str;
+              description = lib.mdDoc "Hostname for the SMTP server";
+            };
+            port = mkOption {
+              type = types.port;
+              description = lib.mdDoc "Port for the SMTP server";
+            };
+            max_conns = mkOption {
+              type = types.int;
+              description = lib.mdDoc
+                "Maximum number of simultaneous connections, defaults to 1";
+              default = 1;
+            };
+            tls_type = mkOption {
+              type = types.enum [ "none" "STARTTLS" "TLS" ];
+              description =
+                lib.mdDoc "Type of TLS authentication with the SMTP server";
+            };
+          };
+        });
+
+        description = lib.mdDoc "List of outgoing SMTP servers";
+      };
+
+      # TODO: refine this type based on the smtp one.
+      "bounce.mailboxes" = mkOption {
+        type = listOf
+          (submodule { freeformType = with types; oneOf [ str int bool ]; });
+        default = [ ];
+        description = lib.mdDoc "List of bounce mailboxes";
+      };
+
+      messengers = mkOption {
+        type = listOf str;
+        default = [ ];
+        description = lib.mdDoc
+          "List of messengers, see: <https://github.com/knadh/listmonk/blob/master/models/settings.go#L64-L74> for options.";
+      };
+    };
+  };
+in {
+  ###### interface
+  options = {
+    services.listmonk = {
+      enable = mkEnableOption
+        (lib.mdDoc "Listmonk, this module assumes a reverse proxy to be set");
+      database = {
+        createLocally = mkOption {
+          type = types.bool;
+          default = false;
+          description = lib.mdDoc
+            "Create the PostgreSQL database and database user locally.";
+        };
+
+        settings = mkOption {
+          default = null;
+          type = with types; nullOr (submodule databaseSettingsOpts);
+          description = lib.mdDoc
+            "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.";
+        };
+        mutableSettings = mkOption {
+          type = types.bool;
+          default = true;
+          description = lib.mdDoc ''
+            Database settings will be reset to the value set in this module if this is not enabled.
+            Enable this if you want to persist changes you have done in the application.
+          '';
+        };
+      };
+      package = mkPackageOption pkgs "listmonk" {};
+      settings = mkOption {
+        type = types.submodule { freeformType = tomlFormat.type; };
+        description = lib.mdDoc ''
+          Static settings set in the config.toml, see <https://github.com/knadh/listmonk/blob/master/config.toml.sample> for details.
+          You can set secrets using the secretFile option with environment variables following <https://listmonk.app/docs/configuration/#environment-variables>.
+        '';
+      };
+      secretFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = lib.mdDoc
+          "A file containing secrets as environment variables. See <https://listmonk.app/docs/configuration/#environment-variables> for details on supported values.";
+      };
+    };
+  };
+
+  ###### implementation
+  config = mkIf cfg.enable {
+    # Default parameters from https://github.com/knadh/listmonk/blob/master/config.toml.sample
+    services.listmonk.settings."app".address = mkDefault "localhost:9000";
+    services.listmonk.settings."db" = mkMerge [
+      ({
+        max_open = mkDefault 25;
+        max_idle = mkDefault 25;
+        max_lifetime = mkDefault "300s";
+      })
+      (mkIf cfg.database.createLocally {
+        host = mkDefault "/run/postgresql";
+        port = mkDefault 5432;
+        user = mkDefault "listmonk";
+        database = mkDefault "listmonk";
+      })
+    ];
+
+    services.postgresql = mkIf cfg.database.createLocally {
+      enable = true;
+
+      ensureUsers = [{
+        name = "listmonk";
+        ensurePermissions = { "DATABASE listmonk" = "ALL PRIVILEGES"; };
+      }];
+
+      ensureDatabases = [ "listmonk" ];
+    };
+
+    systemd.services.listmonk = {
+      description = "Listmonk - newsletter and mailing list manager";
+      after = [ "network.target" ]
+        ++ optional cfg.database.createLocally "postgresql.service";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "exec";
+        EnvironmentFile = mkIf (cfg.secretFile != null) [ cfg.secretFile ];
+        ExecStartPre = [
+          # StateDirectory cannot be used when DynamicUser = true is set this way.
+          # Indeed, it will try to create all the folders and realize one of them already exist.
+          # Therefore, we have to create it ourselves.
+          ''${pkgs.coreutils}/bin/mkdir -p "''${STATE_DIRECTORY}/listmonk/uploads"''
+          "${cfg.package}/bin/listmonk --config ${cfgFile} --idempotent --install --upgrade --yes"
+          "${updateDatabaseConfigScript}/bin/update-database-config.sh"
+        ];
+        ExecStart = "${cfg.package}/bin/listmonk --config ${cfgFile}";
+
+        Restart = "on-failure";
+
+        StateDirectory = [ "listmonk" ];
+
+        User = "listmonk";
+        Group = "listmonk";
+        DynamicUser = true;
+        NoNewPrivileges = true;
+        CapabilityBoundingSet = "";
+        SystemCallArchitecture = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" "@resources" ];
+        ProtectDevices = true;
+        ProtectControlGroups = true;
+        ProtectKernelTunables = true;
+        ProtectHome = true;
+        DeviceAllow = false;
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        UMask = "0027";
+        MemoryDenyWriteExecute = true;
+        LockPersonality = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        ProtectKernelModules = true;
+        PrivateUsers = true;
+      };
+    };
+  };
+}