nixos/listmonk: init module
This commit is contained in:
parent
f3b7d6414b
commit
6b891f4788
@ -266,6 +266,13 @@
|
||||
<link xlink:href="options.html#opt-services.writefreely.enable">services.writefreely</link>.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<link xlink:href="https://listmonk.app">Listmonk</link>, a
|
||||
self-hosted newsletter manager. Enable using
|
||||
<link xlink:href="options.html#opt-services.listmonk.enable">services.listmonk</link>.
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
</section>
|
||||
<section xml:id="sec-release-22.11-incompatibilities">
|
||||
|
@ -94,6 +94,8 @@ Available as [services.patroni](options.html#opt-services.patroni.enable).
|
||||
|
||||
- [WriteFreely](https://writefreely.org), a simple blogging platform with ActivityPub support. Available as [services.writefreely](options.html#opt-services.writefreely.enable).
|
||||
|
||||
- [Listmonk](https://listmonk.app), a self-hosted newsletter manager. Enable using [services.listmonk](options.html#opt-services.listmonk.enable).
|
||||
|
||||
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
|
||||
|
||||
## Backward Incompatibilities {#sec-release-22.11-incompatibilities}
|
||||
|
@ -503,6 +503,7 @@
|
||||
./services/mail/dovecot.nix
|
||||
./services/mail/dspam.nix
|
||||
./services/mail/exim.nix
|
||||
./services/mail/listmonk.nix
|
||||
./services/mail/maddy.nix
|
||||
./services/mail/mail.nix
|
||||
./services/mail/mailcatcher.nix
|
||||
|
222
nixos/modules/services/mail/listmonk.nix
Normal file
222
nixos/modules/services/mail/listmonk.nix
Normal file
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
@ -289,6 +289,7 @@ in {
|
||||
lightdm = handleTest ./lightdm.nix {};
|
||||
lighttpd = handleTest ./lighttpd.nix {};
|
||||
limesurvey = handleTest ./limesurvey.nix {};
|
||||
listmonk = handleTest ./listmonk.nix {};
|
||||
litestream = handleTest ./litestream.nix {};
|
||||
locate = handleTest ./locate.nix {};
|
||||
login = handleTest ./login.nix {};
|
||||
|
69
nixos/tests/listmonk.nix
Normal file
69
nixos/tests/listmonk.nix
Normal file
@ -0,0 +1,69 @@
|
||||
import ./make-test-python.nix ({ lib, ... }: {
|
||||
name = "listmonk";
|
||||
meta.maintainers = with lib.maintainers; [ raitobezarius ];
|
||||
|
||||
nodes.machine = { pkgs, ... }: {
|
||||
services.mailhog.enable = true;
|
||||
services.listmonk = {
|
||||
enable = true;
|
||||
settings = {
|
||||
admin_username = "listmonk";
|
||||
admin_password = "hunter2";
|
||||
};
|
||||
database = {
|
||||
createLocally = true;
|
||||
# https://github.com/knadh/listmonk/blob/174a48f252a146d7e69dab42724e3329dbe25ebe/internal/messenger/email/email.go#L18-L27
|
||||
settings.smtp = [ {
|
||||
enabled = true;
|
||||
host = "localhost";
|
||||
port = 1025;
|
||||
tls_type = "none";
|
||||
}];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import json
|
||||
|
||||
start_all()
|
||||
|
||||
basic_auth = "listmonk:hunter2"
|
||||
def generate_listmonk_request(type, url, data=None):
|
||||
if data is None: data = {}
|
||||
json_data = json.dumps(data)
|
||||
return f'curl -u "{basic_auth}" -X {type} "http://localhost:9000/api/{url}" -H "Content-Type: application/json; charset=utf-8" --data-raw \'{json_data}\'''
|
||||
|
||||
machine.wait_for_unit("mailhog.service")
|
||||
machine.wait_for_unit("postgresql.service")
|
||||
machine.wait_for_unit("listmonk.service")
|
||||
machine.wait_for_open_port(1025)
|
||||
machine.wait_for_open_port(8025)
|
||||
machine.wait_for_open_port(9000)
|
||||
machine.succeed("[[ -f /var/lib/listmonk/.db_settings_initialized ]]")
|
||||
|
||||
# Test transactional endpoint
|
||||
# subscriber_id=1 is guaranteed to exist at install-time
|
||||
# template_id=2 is guaranteed to exist at install-time and is a transactional template (1 is a campaign template).
|
||||
machine.succeed(
|
||||
generate_listmonk_request('POST', 'tx', data={'subscriber_id': 1, 'template_id': 2})
|
||||
)
|
||||
assert 'Welcome John Doe' in machine.succeed(
|
||||
"curl --fail http://localhost:8025/api/v2/messages"
|
||||
)
|
||||
|
||||
# Test campaign endpoint
|
||||
# Based on https://github.com/knadh/listmonk/blob/174a48f252a146d7e69dab42724e3329dbe25ebe/cmd/campaigns.go#L549 as docs do not exist.
|
||||
campaign_data = json.loads(machine.succeed(
|
||||
generate_listmonk_request('POST', 'campaigns/1/test', data={'template_id': 1, 'subscribers': ['john@example.com'], 'name': 'Test', 'subject': 'NixOS is great', 'lists': [1], 'messenger': 'email'})
|
||||
))
|
||||
|
||||
assert campaign_data['data'] # This is a boolean asserting if the test was successful or not: https://github.com/knadh/listmonk/blob/174a48f252a146d7e69dab42724e3329dbe25ebe/cmd/campaigns.go#L626
|
||||
|
||||
messages = json.loads(machine.succeed(
|
||||
"curl --fail http://localhost:8025/api/v2/messages"
|
||||
))
|
||||
|
||||
assert messages['total'] == 2
|
||||
'';
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
{ lib, buildGoModule, fetchFromGitHub, callPackage, stuffbin }:
|
||||
{ lib, buildGoModule, fetchFromGitHub, callPackage, stuffbin, nixosTests }:
|
||||
|
||||
buildGoModule rec {
|
||||
pname = "listmonk";
|
||||
@ -43,6 +43,7 @@ buildGoModule rec {
|
||||
|
||||
passthru = {
|
||||
frontend = callPackage ./frontend.nix { inherit meta; };
|
||||
tests = { inherit (nixosTests) listmonk; };
|
||||
};
|
||||
|
||||
meta = with lib; {
|
||||
|
Loading…
Reference in New Issue
Block a user