Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/automx2: multi-domain support, service improvements, configurability #370074

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
279 changes: 217 additions & 62 deletions nixos/modules/services/mail/automx2.nix
SuperSandro2000 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,64 @@
let
cfg = config.services.automx2;
format = pkgs.formats.json { };
imapSmtpServerType = lib.types.mkOptionType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

automx2 also support 'pop' as server type.

name = "imapSmtpServerType";
description = "automx2 settings: IMAP/SMTP server configuration";
check =
x:
if !builtins.isAttrs x then
false
else if !lib.types.str.check x.type then
false
else if x.type != "imap" && x.type != "smtp" then
false
else if !lib.types.str.check x.name then
false
else if !lib.types.port.check x.port then
false
else
true;
Comment on lines +14 to +27
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you refer to the individual checks or the overall approach?

  • individual checks: I'll add comments to make them easier to understand
  • general approach: my goal was, to properly validate the JSON datastructure that needs to be passed to /initdb to catch issues at evaluation instead of at runtime with a failing service startup…
    • Is there a better way than using custom types?
    • Is my approach of using type checks wrong?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general approach I must admit. It would be cool, if we could just validate the config with the upstream program. That would make maintenance a lot easier.

};
davServerType = lib.types.mkOptionType {
name = "davServerType";
description = "automx2 settings: CalDAV/CardDAV server configuration";
check =
x:
if !builtins.isAttrs x then
false
else if
!builtins.all (key: builtins.hasAttr key x) [
"type"
"url"
"port"
]
then
false
else if !lib.types.str.check x.type then
false
else if x.type != "caldav" && x.type != "carddav" then
false
else if !lib.types.str.check x.url then
false
else if !lib.types.port.check x.port then
false
else
true;
SuperSandro2000 marked this conversation as resolved.
Show resolved Hide resolved
};
serverType = lib.types.mkOptionType {
name = "serverType";
description = "The autoconfig values of mail and/or DAV services";
check =
x:
if !lib.isList x then
false
else if builtins.length x < 1 then
false
else if !lib.all (item: imapSmtpServerType.check item || davServerType.check item) x then
false
else
true;
SuperSandro2000 marked this conversation as resolved.
Show resolved Hide resolved
};
in
{
options = {
Expand All @@ -19,11 +77,14 @@ in
"automx2"
] { };

domain = lib.mkOption {
type = lib.types.str;
example = "example.com";
domains = lib.mkOption {
type = lib.types.nonEmptyListOf lib.types.str;
example = [
"example.org"
"example.com"
];
description = ''
E-Mail-Domain for which mail client autoconfig/autoconfigure should be set up.
E-Mail-Domains for which mail client autoconfig/autoconfigure should be set up.
The `autoconfig` and `autodiscover` subdomains are automatically prepended and set up with ACME.
The names of those domains are hardcoded in the mail clients and are not configurable.
'';
Expand All @@ -36,73 +97,167 @@ in
};

settings = lib.mkOption {
inherit (format) type;
description = ''
Bootstrap json to populate database.
See [docs](https://rseichter.github.io/automx2/#_sqlite) for details.
'';
};
};
};
description = "Configuration of data provided by the automx2 service. Used to populate DB at service startup. See [docs](https://rseichter.github.io/automx2/#_sqlite) for details.";
type = lib.types.submodule {
freeformType = format.type;
options = {
provider = lib.mkOption {
type = lib.types.str;
example = "ACME Corp & Brothers Communication Services";
description = "A description letting the user know, who provides the service.";
};

config = lib.mkIf cfg.enable {
services.nginx = {
enable = true;
virtualHosts = {
"autoconfig.${cfg.domain}" = {
enableACME = true;
forceSSL = true;
serverAliases = [ "autodiscover.${cfg.domain}" ];
locations = {
"/".proxyPass = "http://127.0.0.1:${toString cfg.port}/";
"/initdb".extraConfig = ''
# Limit access to clients connecting from localhost
allow 127.0.0.1;
deny all;
'';
domains = lib.mkOption {
type = lib.types.nonEmptyListOf lib.types.str;
description = "The domains for which automx2 provides an autoconfiguration service";
default = cfg.domains;
defaultText = lib.literalExpression "services.automx2.domains";
example = [
"example.org"
"example.com"
];
};

servers = lib.mkOption {
type = serverType;
description = "The offered services and their connection details";
example = [
{
name = "mail.example.org";
port = 993;
type = "imap";
}
{
url = "https://dav.example.com/cal/dav/";
port = 443;
type = "carddav";
}
];
default = [ ];
};
};
};
};
};

systemd.services.automx2 = {
after = [ "network.target" ];
postStart = ''
sleep 3
${lib.getExe pkgs.curl} -X POST --json @${format.generate "automx2.json" cfg.settings} http://127.0.0.1:${toString cfg.port}/initdb/
'';
serviceConfig = {
Environment = [
"AUTOMX2_CONF=${pkgs.writeText "automx2-conf" ''
[automx2]
loglevel = WARNING
db_uri = sqlite:///:memory:
proxy_count = 1
''}"
"FLASK_APP=automx2.server:app"
"FLASK_CONFIG=production"
logLevel = lib.mkOption {
type = lib.types.enum [
"INFO"
"WARNING"
"DEBUG"
"ERROR"
"CRITICAL"
];
ExecStart = "${
pkgs.python3.buildEnv.override { extraLibs = [ cfg.package ]; }
}/bin/flask run --host=127.0.0.1 --port=${toString cfg.port}";
Restart = "always";
StateDirectory = "automx2";
User = "automx2";
WorkingDirectory = "/var/lib/automx2";
default = "WARNING";
description = "Log level used for automx2 service.";
example = "DEBUG";
};
unitConfig = {
Description = "MUA configuration service";
Documentation = "https://rseichter.github.io/automx2/";
};
wantedBy = [ "multi-user.target" ];
};

users = {
groups.automx2 = { };
users.automx2 = {
group = "automx2";
isSystemUser = true;
webserver = lib.mkOption {
type = lib.types.enum [
"nginx"
"caddy"
];
default = "nginx";
description = ''
Whether to use nginx or caddy for virtual host management.

Further nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`.
See [](#opt-services.nginx.virtualHosts) for further information.

Further caddy configuration can be done by adapting `services.caddy.virtualHosts.<name>`.
See [](#opt-services.caddy.virtualHosts) for further information.
'';
};

};
};

config = lib.mkIf cfg.enable (
lib.mkMerge [
{
systemd.services.automx2 = {
after = [ "network.target" ];
postStart = "${lib.getExe pkgs.curl} -X POST --json @${format.generate "automx2.json" cfg.settings} http://127.0.0.1:${toString cfg.port}/initdb/";
serviceConfig = {
Environment = [
"AUTOMX2_CONF=${pkgs.writeText "automx2-conf" ''
[automx2]
loglevel = ${cfg.logLevel}
db_uri = sqlite:///:memory:
proxy_count = 1
''}"
"FLASK_APP=automx2.server:app"
"FLASK_CONFIG=production"
];
ExecStart = "${
pkgs.python3.buildEnv.override { extraLibs = [ cfg.package ]; }
}/bin/flask run --host=127.0.0.1 --port=${toString cfg.port}";
Restart = "always";
DynamicUser = true;
User = "automx2";
Type = "notify";
};
unitConfig = {
Description = "Service to automatically configure mail clients";
Documentation = "https://rseichter.github.io/automx2/";
};
wantedBy = [ "multi-user.target" ];
};
}
(lib.mkIf (cfg.webserver == "nginx") {
services.nginx = {
enable = true;
virtualHosts = builtins.listToAttrs (
map (domain: {
name = "autoconfig.${domain}";
value = {
enableACME = true;
forceSSL = true;
serverAliases = [ "autodiscover.${domain}" ];
locations = {
"/".proxyPass = "http://127.0.0.1:${toString cfg.port}/";
# TODO: verify this actually blocks external requests due to the current IP/proxy issue?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# TODO: verify this actually blocks external requests due to the current IP/proxy issue?

it does, see https://autoconfig.c3d2.de/initdb

as long as your not chaining proxies.

"/initdb".extraConfig = ''
# Limit access to clients connecting from localhost
allow 127.0.0.1;
deny all;
'';
};
};
}) cfg.domains
);
};
})
(lib.mkIf (cfg.webserver == "caddy") {
services.caddy = {
enable = true;
virtualHosts = builtins.listToAttrs (
map (domain: {
name = "autoconfig.${domain}";
value = {
serverAliases = [ "autodiscover.${domain}" ];
extraConfig = ''
route /initdb* {
respond 403 {
body "Access Denied"
}
}

route * {
reverse_proxy http://127.0.0.1:${toString cfg.port}
}
'';
};
}) cfg.domains
);
};
})
]
);

imports = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: Normally we place imports between options and config

(lib.mkChangedOptionModule [ "services" "automx2" "domain" ] [ "services" "automx2" "domains" ]
(config: [ config.services.automx2.domain ])
)
];
}
5 changes: 3 additions & 2 deletions pkgs/development/python-modules/automx2/default.nix
eliasp marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
ldap3,
pytestCheckHook,
pythonOlder,
pythonPackages,
eliasp marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pythonPackages,

setuptools,
}:

buildPythonPackage rec {
pname = "automx2";
version = "2024.2";
version = "2025.1";
pyproject = true;

disabled = pythonOlder "3.7";
Expand All @@ -21,7 +22,7 @@ buildPythonPackage rec {
owner = "rseichter";
repo = "automx2";
tag = version;
hash = "sha256-7SbSKSjDHTppdqfPPKvuWbdoksHa6BMIOXOq0jDggTE=";
hash = "sha256-EG0S8Ie9U1nV96th7NdGsbAWXLVoqddHbGdHt/FUlqE=";
};

nativeBuildInputs = [ setuptools ];
Expand Down