diff options
author | Raito Bezarius | 2022-08-30 13:15:41 +0200 |
---|---|---|
committer | Raito Bezarius | 2022-09-21 19:55:20 +0200 |
commit | 6b891f4788a302f9e847e808b32780066b268864 (patch) | |
tree | 12c35388929ccc686c7ac8973445526c1e393e2d /nixos/modules/services/mail/listmonk.nix | |
parent | f3b7d6414b06eb493ed7f258a6938be440b7cdaf (diff) |
nixos/listmonk: init module
Diffstat (limited to 'nixos/modules/services/mail/listmonk.nix')
-rw-r--r-- | nixos/modules/services/mail/listmonk.nix | 222 |
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; + }; + }; + }; +} |