diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix index be0b87eb7642f..c866f1b7c062f 100644 --- a/nixos/modules/services/web-apps/nextcloud.nix +++ b/nixos/modules/services/web-apps/nextcloud.nix @@ -120,7 +120,8 @@ let ++ (lib.optional ( cfg.config.objectstore.s3.sseCKeyFile != null ) "s3_sse_c_key:${cfg.config.objectstore.s3.sseCKeyFile}") - ++ (lib.optional (cfg.secretFile != null) "secret_file:${cfg.secretFile}"); + ++ (lib.optional (cfg.secretFile != null) "secret_file:${cfg.secretFile}") + ++ (lib.mapAttrsToList (credential: file: "${credential}:${file}") cfg.secrets); requiresRuntimeSystemdCredentials = (lib.length runtimeSystemdCredentials) != 0; @@ -296,6 +297,9 @@ let ) "'dbtableprefix' => '${toString c.dbtableprefix}',"} ${lib.optionalString (c.dbpassFile != null) "'dbpassword' => nix_read_secret('dbpass'),"} 'dbtype' => '${c.dbtype}', + ${lib.concatStringsSep "\n" ( + lib.mapAttrsToList (name: credential: "'${name}' => nix_read_secret('${name}'),") cfg.secrets + )} ${objectstoreConfig} ]; @@ -390,6 +394,24 @@ in ''; example = "/mnt/nextcloud-file"; }; + secrets = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.pathWith { + inStore = false; + absolute = true; + } + ); + default = { }; + description = '' + Secret files to read into entries in `config.php`. + This uses `nix_read_secret` and LoadCredential to read the contents of the file into the entry in `config.php`. + ''; + example = lib.literalExpression '' + { + oidc_login_client_secret = "/run/secrets/nextcloud_oidc_secret"; + } + ''; + }; extraApps = lib.mkOption { type = lib.types.attrsOf lib.types.package; default = { }; @@ -975,6 +997,126 @@ in The preview providers that should be explicitly enabled. ''; }; + mail_domain = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + The return address that you want to appear on emails sent by the Nextcloud server, for example `nc-admin@example.com`, substituting your own domain, of course. + ''; + }; + mail_from_address = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + FROM address that overrides the built-in `sharing-noreply` and `lostpassword-noreply` FROM addresses. + Defaults to different FROM addresses depending on the feature. + ''; + }; + mail_smtpdebug = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Enable SMTP class debugging. + `loglevel` will likely need to be adjusted too. + [See docs](https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/email_configuration.html#enabling-debug-mode). + ''; + }; + mail_smtpmode = lib.mkOption { + type = lib.types.enum [ + "sendmail" + "smtp" + "qmail" + "null" # Yes, this is really a string null and not null. + ]; + default = "smtp"; + description = '' + Which mode to use for sending mail. + If you are using local or remote SMTP, set this to `smtp`. + For the `sendmail` option, you need an installed and working email system on the server, with your local `sendmail` installation. + For `qmail`, the binary is /var/qmail/bin/sendmail, and it must be installed on your Unix system. + Use the string null to send no mails (disable mail delivery). This can be useful if mails should be sent via APIs and rendering messages is not necessary. + ''; + }; + mail_smtphost = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = '' + This depends on `mail_smtpmode`. Specify the IP address of your mail server host. This may contain multiple hosts separated by a semicolon. If you need to specify the port number, append it to the IP address separated by a colon, like this: `127.0.0.1:24`. + ''; + }; + mail_smtpport = lib.mkOption { + type = lib.types.port; + default = 25; + description = '' + This depends on `mail_smtpmode`. Specify the port for sending mail. + ''; + }; + mail_smtptimeout = lib.mkOption { + type = lib.types.int; + default = 10; + description = '' + This depends on `mail_smtpmode`. This sets the SMTP server timeout, in seconds. You may need to increase this if you are running an anti-malware or spam scanner. + ''; + }; + mail_smtpsecure = lib.mkOption { + type = lib.types.enum [ + "" + "ssl" + ]; + default = ""; + description = '' + This depends on `mail_smtpmode`. Specify `ssl` when you are using SSL/TLS. Any other value will be ignored. + If the server advertises STARTTLS capabilities, they might be used, but they cannot be enforced by this config option. + ''; + }; + mail_smtpauth = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + This depends on `mail_smtpmode`. Change this to `true` if your mail server requires authentication. + ''; + }; + mail_smtpname = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + This depends on `mail_smtpauth`. Specify the username for authenticating to the SMTP server. + ''; + }; + # mail_smtppassword is skipped as it must be set through services.nextcloud.secrets + mail_template_class = lib.mkOption { + type = lib.types.str; + default = "\\OC\\Mail\\EMailTemplate"; + description = '' + Replaces the default mail template layout. This can be utilized if the options to modify the mail texts with the theming app are not enough. + The class must extend `\OC\Mail\EMailTemplate` + ''; + }; + mail_send_plaintext_only = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Email will be sent by default with an HTML and a plain text body. This option allows sending only plain text emails. + ''; + }; + mail_smtpstreamoptions = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + This depends on `mail_smtpmode`. Array of additional streams options that will be passed to underlying Swift mailer implementation. + ''; + }; + mail_sendmailmode = lib.mkOption { + type = lib.types.enum [ + "smtp" + "pipe" + ]; + default = "smtp"; + description = '' + For `smtp`, the sendmail binary is started with the parameter `-bs`: Use the SMTP protocol on standard input and output. + For `pipe`, the binary is started with the parameters `-t`: Read message from STDIN and extract recipients. + ''; + }; }; }; default = { }; @@ -1165,6 +1307,13 @@ in If `services.nextcloud.config.adminpassFile` is null, `services.nextcloud.config.adminuser` must be null as well in order to disable initial admin user creation. ''; } + { + assertion = !(cfg.settings ? mail_smtppassword); + message = '' + The option `services.nextcloud.settings.mail_smtppassword` must not be used, as it puts the password into the world-readable nix store. + Use `services.nextcloud.secrets.mail_smtppassword` instead and set it to a file containing the password. + ''; + } ]; } diff --git a/nixos/tests/nextcloud/basic.nix b/nixos/tests/nextcloud/basic.nix index 276465716617f..6062e198269cd 100644 --- a/nixos/tests/nextcloud/basic.nix +++ b/nixos/tests/nextcloud/basic.nix @@ -63,8 +63,11 @@ runTest ( }; phpExtraExtensions = all: [ all.bz2 ]; nginx.enableFastcgiRequestBuffering = true; + secrets.mysecret = "/etc/nextcloud/mysecretfile"; }; + environment.etc."nextcloud/mysecretfile".text = "foobar"; + specialisation.withoutMagick.configuration = { services.nextcloud.enableImagemagick = false; }; @@ -116,6 +119,9 @@ runTest ( client_hash = client.succeed("nix-hash testfile.bin").strip() nextcloud_hash = nextcloud.succeed("nix-hash /var/lib/nextcloud-data/data/root/files/testfile.bin").strip() t.assertEqual(client_hash, nextcloud_hash) + + with subtest("secrets"): + assert "foobar" == nextcloud.succeed("nextcloud-occ config:system:get mysecret").strip() ''; } ) diff --git a/nixos/tests/nextcloud/default.nix b/nixos/tests/nextcloud/default.nix index f452a4bf12181..72f7a28070b5c 100644 --- a/nixos/tests/nextcloud/default.nix +++ b/nixos/tests/nextcloud/default.nix @@ -75,8 +75,7 @@ let inherit (config) test-helpers; in mkBefore '' - nextcloud.start() - client.start() + start_all() nextcloud.wait_for_unit("multi-user.target") ${test-helpers.init} @@ -136,6 +135,7 @@ let ./with-mysql-and-memcached.nix ./with-postgresql-and-redis.nix ./with-objectstore.nix + ./with-mail.nix ] ++ (pkgs.lib.optional (version >= 32) ./without-admin-user.nix) ); diff --git a/nixos/tests/nextcloud/with-mail.nix b/nixos/tests/nextcloud/with-mail.nix new file mode 100644 index 0000000000000..24f1ef2830563 --- /dev/null +++ b/nixos/tests/nextcloud/with-mail.nix @@ -0,0 +1,100 @@ +{ + name, + pkgs, + testBase, + system, + ... +}: +with import ../../lib/testing-python.nix { inherit system pkgs; }; +runTest ( + { config, lib, ... }: + let + certs = import ../common/acme/server/snakeoil-certs.nix; + domain = certs.domain; + in + { + inherit name; + + meta.maintainers = lib.teams.nextcloud.members; + + imports = [ testBase ]; + + nodes = { + nextcloud = + { + config, + pkgs, + nodes, + ... + }: + { + security.pki.certificateFiles = [ certs.ca.cert ]; + + networking.extraHosts = '' + ${nodes.stalwart.networking.primaryIPAddress} ${domain} + ''; + + environment.etc."nextcloud/mail_smtppassword".text = "foobar"; + + services.nextcloud = { + config.dbtype = "sqlite"; + + settings = { + mail_from_address = "alice"; + mail_domain = domain; + mail_smtpmode = "smtp"; + mail_smtphost = domain; + mail_smtpport = 587; + mail_smtpauth = true; + mail_smtpname = "alice"; + mail_send_plaintext_only = true; + }; + + secrets.mail_smtppassword = "/etc/nextcloud/mail_smtppassword"; + }; + }; + + stalwart = + { pkgs, ... }: + { + imports = [ ../stalwart/stalwart-mail-config.nix ]; + + networking.firewall.allowedTCPPorts = [ 587 ]; + + environment.systemPackages = [ + (pkgs.writers.writePython3Bin "test-imap-read" { } '' + from imaplib import IMAP4 + + with IMAP4('localhost') as imap: + imap.starttls() + status, [caps] = imap.login('bob', 'foobar') + assert status == 'OK' + imap.select() + status, [ref] = imap.search(None, 'ALL') + assert status == 'OK' + [msgId] = ref.split() + status, msg = imap.fetch(msgId, 'BODY[TEXT]') + assert status == 'OK' + assert (msg[0][1].strip() + == (b'Well done, ${config.adminuser}!\r\n\r\n' + b'If you received this email, the email configuration ' + b's=\r\neems to be correct.\r\n\r\n\r\n--=20\r\n' + b'Nextcloud - a safe home for all your data=\r\n\r\n' + b'This is an automatically sent email, please do not reply.')) + '') + ]; + }; + }; + + test-helpers.init = '' + stalwart.wait_for_unit("multi-user.target") + stalwart.wait_until_succeeds("nc -vzw 2 localhost 587") + + nextcloud.succeed("nc -vzw 2 ${domain} 587") + nextcloud.succeed("curl -sS --fail-with-body -u ${config.adminuser}:${config.adminpass} -H 'OCS-APIRequest: true' -X PUT http://nextcloud/ocs/v2.php/cloud/users/${config.adminuser} -H 'Content-Type: application/json' --data-raw '{\"key\":\"email\",\"value\":\"bob@${domain}\"}'") + nextcloud.succeed("curl -sS --fail-with-body -u ${config.adminuser}:${config.adminpass} -H 'OCS-APIRequest: true' -X POST http://nextcloud/settings/admin/mailtest") + + stalwart.succeed("test-imap-read") + ''; + } +) diff --git a/nixos/tests/nextcloud/with-objectstore.nix b/nixos/tests/nextcloud/with-objectstore.nix index c652df46b1c4c..44777d6d8ac06 100644 --- a/nixos/tests/nextcloud/with-objectstore.nix +++ b/nixos/tests/nextcloud/with-objectstore.nix @@ -111,7 +111,6 @@ runTest ( }; test-helpers.init = '' - minio.start() minio.wait_for_open_port(9000) minio.wait_for_unit("nginx.service") minio.wait_for_open_port(443)