diff --git a/nixos/modules/services/mail/automx2.nix b/nixos/modules/services/mail/automx2.nix index 2cfb5f491341b..0293ebdec5a58 100644 --- a/nixos/modules/services/mail/automx2.nix +++ b/nixos/modules/services/mail/automx2.nix @@ -8,6 +8,64 @@ let cfg = config.services.automx2; format = pkgs.formats.json { }; + imapSmtpServerType = lib.types.mkOptionType { + 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; + }; + 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; + }; + 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; + }; in { options = { @@ -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. ''; @@ -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.`. + See [](#opt-services.nginx.virtualHosts) for further information. + + Further caddy configuration can be done by adapting `services.caddy.virtualHosts.`. + 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? + "/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 = [ + (lib.mkChangedOptionModule [ "services" "automx2" "domain" ] [ "services" "automx2" "domains" ] + (config: [ config.services.automx2.domain ]) + ) + ]; } diff --git a/pkgs/development/python-modules/automx2/default.nix b/pkgs/development/python-modules/automx2/default.nix index 8b3604c4e2c22..1f08f26f5a81f 100644 --- a/pkgs/development/python-modules/automx2/default.nix +++ b/pkgs/development/python-modules/automx2/default.nix @@ -7,12 +7,13 @@ ldap3, pytestCheckHook, pythonOlder, + pythonPackages, setuptools, }: buildPythonPackage rec { pname = "automx2"; - version = "2024.2"; + version = "2025.1"; pyproject = true; disabled = pythonOlder "3.7"; @@ -21,7 +22,7 @@ buildPythonPackage rec { owner = "rseichter"; repo = "automx2"; tag = version; - hash = "sha256-7SbSKSjDHTppdqfPPKvuWbdoksHa6BMIOXOq0jDggTE="; + hash = "sha256-EG0S8Ie9U1nV96th7NdGsbAWXLVoqddHbGdHt/FUlqE="; }; nativeBuildInputs = [ setuptools ];