diff --git a/doc/guix.texi b/doc/guix.texi index 111b911d00..9bbf85e32b 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -28248,23 +28248,195 @@ Mailutils Manual}, for details. @cindex CardDAV @defvar radicale-service-type -This is the type of the @uref{https://radicale.org, Radicale} CalDAV/CardDAV -server whose value should be a @code{radicale-configuration}. +This is the type of the @uref{https://radicale.org, Radicale} +CalDAV/CardDAV server whose value should be a +@code{radicale-configuration}. The default configuration matches the +@uref{https://radicale.org/v3.html#configuration, upstream +documentation}. @end defvar @deftp {Data Type} radicale-configuration Data type representing the configuration of @command{radicale}. +Available @code{radicale-configuration} fields are: @table @asis -@item @code{package} (default: @code{radicale}) -The package that provides @command{radicale}. +@item @code{package} (default: @code{radicale}) (type: package) +Package that provides @command{radicale}. -@item @code{config-file} (default: @code{%default-radicale-config-file}) -File-like object of the configuration file to use, by default it will listen -on TCP port 5232 of @code{localhost} and use the @code{htpasswd} file at -@file{/var/lib/radicale/users} with no (@code{plain}) encryption. +@item @code{auth} (default: @code{'()}) (type: radicale-auth-configuration) +Configuration for auth-related variables. + +@deftp {Data Type} radicale-auth-configuration +Data type representing the @code{auth} section of a @command{radicale} +configuration file. Available @code{radicale-auth-configuration} fields +are: + +@table @asis +@item @code{type} (default: @code{'none}) (type: symbol) +The method to verify usernames and passwords. Options are @code{none}, +@code{htpasswd}, @code{remote-user}, and @code{http-x-remote-user}. +This value is tied to @code{htpasswd-filename} and +@code{htpasswd-encryption}. + +@item @code{htpasswd-filename} (default: @code{"/etc/radicale/users"}) (type: file-name) +Path to the htpasswd file. Use htpasswd or similar to generate this +file. + +@item @code{htpasswd-encryption} (default: @code{'md5}) (type: symbol) +Encryption method used in the htpasswd file. Options are @code{plain}, +@code{bcrypt}, and @code{md5}. + +@item @code{delay} (default: @code{1}) (type: non-negative-integer) +Average delay after failed login attempts in seconds. + +@item @code{realm} (default: @code{"Radicale - Password Required"}) (type: string) +Message displayed in the client when a password is needed. @end table + +@end deftp + +@item @code{encoding} (default: @code{'()}) (type: radicale-encoding-configuration) +Configuration for encoding-related variables. + +@deftp {Data Type} radicale-encoding-configuration +Data type representing the @code{encoding} section of a +@command{radicale} configuration file. Available +@code{radicale-encoding-configuration} fields are: + +@table @asis +@item @code{request} (default: @code{'utf-8}) (type: symbol) +Encoding for responding requests. + +@item @code{stock} (default: @code{'utf-8}) (type: symbol) +Encoding for storing local collections. + +@end table + +@end deftp + +@item @code{headers-file} (default: none) (type: file-like) +Custom HTTP headers. + +@item @code{logging} (default: @code{'()}) (type: radicale-logging-configuration) +Configuration for logging-related variables. + +@deftp {Data Type} radicale-logging-configuration +Data type representing the @code{logging} section of a +@command{radicale} configuration file. Available +@code{radicale-logging-configuration} fields are: + +@table @asis +@item @code{level} (default: @code{'warning}) (type: symbol) +Set the logging level. One of @code{debug}, @code{info}, +@code{warning}, @code{error}, or @code{critical}. + +@item @code{mask-passwords?} (default: @code{#t}) (type: boolean) +Whether to include passwords in logs. + +@end table + +@end deftp + +@item @code{rights} (default: @code{'()}) (type: radicale-rights-configuration) +Configuration for rights-related variables. This should be a +@code{radicale-rights-configuration}. + +@deftp {Data Type} radicale-rights-configuration +Data type representing the @code{rights} section of a @command{radicale} +configuration file. Available @code{radicale-rights-configuration} +fields are: + +@table @asis +@item @code{type} (default: @code{'owner-only}) (type: symbol) +Backend used to check collection access rights. The recommended backend +is @code{owner-only}. If access to calendars and address books outside +the home directory of users is granted, clients won't detect these +collections and will not show them to the user. Choosing any other +method is only useful if you access calendars and address books directly +via URL. Options are @code{authenticate}, @code{owner-only}, +@code{owner-write}, and @code{from-file}. + +@item @code{file} (default: @code{""}) (type: file-name) +File for the rights backend @code{from-file}. + +@end table + +@end deftp + +@item @code{server} (default: @code{'()}) (type: radicale-server-configuration) +Configuration for server-related variables. Ignored if WSGI is used. + +@deftp {Data Type} radicale-server-configuration +Data type representing the @code{server} section of a @command{radicale} +configuration file. Available @code{radicale-server-configuration} +fields are: + +@table @asis +@item @code{hosts} (default: @code{(list "localhost:5232")}) (type: list-of-ip-addresses) +List of IP addresses that the server will bind to. + +@item @code{max-connections} (default: @code{8}) (type: non-negative-integer) +Maximum number of parallel connections. Set to 0 to disable the limit. + +@item @code{max-content-length} (default: @code{100000000}) (type: non-negative-integer) +Maximum size of the request body in bytes. + +@item @code{timeout} (default: @code{30}) (type: non-negative-integer) +Socket timeout in seconds. + +@item @code{ssl?} (default: @code{#f}) (type: boolean) +Whether to enable transport layer encryption. + +@item @code{certificate} (default: @code{"/etc/ssl/radicale.cert.pem"}) (type: file-name) +Path of the SSL certificate. + +@item @code{key} (default: @code{"/etc/ssl/radicale.key.pem"}) (type: file-name) +Path to the private key for SSL. Only effective if @code{ssl?} is +@code{#t}. + +@item @code{certificate-authority} (default: @code{""}) (type: file-name) +Path to CA certificate for validating client certificates. This can be +used to secure TCP traffic between Radicale and a reverse proxy. If you +want to authenticate users with client-side certificates, you also have +to write an authentication plugin that extracts the username from the +certificate. + +@end table + +@end deftp + +@item @code{storage} (default: @code{'()}) (type: radicale-storage-configuration) +Configuration for storage-related variables. + +@deftp {Data Type} radicale-storage-configuration +Data type representing the @code{storage} section of a +@command{radicale} configuration file. Available +@code{radicale-storage-configuration} fields are: + +@table @asis +@item @code{type} (default: @code{'multifilesystem}) (type: symbol) +Backend used to store data. Options are @code{multifilesystem} and +@code{multifilesystem-nolock}. + +@item @code{filesystem-folder} (default: @code{"/var/lib/radicale/collections"}) (type: file-name) +Folder for storing local collections. Created if not present. + +@item @code{max-sync-token-age} (default: @code{2592000}) (type: non-negative-integer) +Delete sync-tokens that are older than the specified time in seconds. + +@item @code{hook} (default: @code{""}) (type: string) +Command run after changes to storage. + +@end table + +@end deftp + +@item @code{web-interface?} (default: @code{#t}) (type: boolean) +Whether to use Radicale's built-in web interface. + +@end table + @end deftp @subsubheading Rspamd Service diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm index afe1bb6016..9b4bfd360f 100644 --- a/gnu/services/mail.scm +++ b/gnu/services/mail.scm @@ -7,6 +7,7 @@ ;;; Copyright © 2020 Jonathan Brielmaier ;;; Copyright © 2023 Thomas Ieong ;;; Copyright © 2023 Saku Laesvuori +;;; Copyright © 2024 Juliana Sims ;;; ;;; This file is part of GNU Guix. ;;; @@ -38,10 +39,12 @@ (define-module (gnu services mail) #:use-module (gnu packages dav) #:use-module (gnu packages tls) #:use-module (guix deprecation) + #:use-module ((guix diagnostics) #:select (source-properties->location)) #:use-module (guix modules) #:use-module (guix records) #:use-module (guix packages) #:use-module (guix gexp) + #:use-module (ice-9 curried-definitions) #:use-module (ice-9 match) #:use-module (ice-9 format) #:use-module (srfi srfi-1) @@ -79,10 +82,21 @@ (define-module (gnu services mail) imap4d-service-type %default-imap4d-config-file + radicale-auth-configuration + radicale-auth-configuration? + radicale-encoding-configuration + radicale-encoding-configuration? + radicale-logging-configuration + radicale-logging-configuration? + radicale-rights-configuration + radicale-rights-configuration? + radicale-server-configuration + radicale-server-configuration? + radicale-storage-configuration + radicale-storage-configuration? radicale-configuration radicale-configuration? radicale-service-type - %default-radicale-config-file rspamd-configuration rspamd-service-type @@ -1929,23 +1943,258 @@ (define imap4d-service-type ;;; Radicale. ;;; -(define-record-type* - radicale-configuration make-radicale-configuration - radicale-configuration? - (package radicale-configuration-package - (default radicale)) - (config-file radicale-configuration-config-file - (default %default-radicale-config-file))) +;; Maybe types -(define %default-radicale-config-file - (plain-file "radicale.conf" " -[auth] -type = htpasswd -htpasswd_filename = /var/lib/radicale/users -htpasswd_encryption = plain +(define (comma-separated-ip-list? lst) + (every (lambda (s) + (or (string-prefix? "localhost" s) + ((@@ (gnu services vpn) ipv4-address?) s) + ((@@ (gnu services vpn) ipv6-address?) s))) + lst)) -[server] -hosts = localhost:5232")) +(define-maybe boolean (prefix radicale-)) +(define-maybe comma-separated-ip-list (prefix radicale-)) +(define-maybe file-name (prefix radicale-)) +(define-maybe non-negative-integer (prefix radicale-)) +(define-maybe string (prefix radicale-)) +(define-maybe symbol (prefix radicale-)) + +;; Serializers and sanitizers + +(define (radicale-serialize-field field-name value) + ;; XXX We quote the un-gexp form here because otherwise symbol-literals are + ;; treated as variables. We can get away with this because all of our other + ;; field value types are primitives by the time they get here so are printed + ;; the same whether or not they are quoted. + #~(format #f "~a = ~a\n" #$(uglify-field-name field-name) '#$value)) + +(define (radicale-serialize-boolean field-name value?) + (radicale-serialize-field field-name (if value? "True" "False"))) + +(define (radicale-serialize-comma-separated-ip-list field-name value) + (radicale-serialize-field field-name (string-join value ", "))) + +(define radicale-serialize-file-name radicale-serialize-field) + +(define radicale-serialize-non-negative-integer radicale-serialize-field) + +(define radicale-serialize-string radicale-serialize-field) + +(define radicale-serialize-symbol radicale-serialize-field) + +(define ((sanitize-delimited-symbols syms location field) value) + (cond + ((not (maybe-value-set? value)) + value) + ((member value syms) + (string->symbol (uglify-field-name value))) + (else + (configuration-field-error (source-properties->location location) + field + value)))) + +;; Section configuration types + +(define-configuration radicale-auth-configuration + (type + maybe-symbol + "The method to verify usernames and passwords. Options are @code{none}, +@code{htpasswd}, @code{remote-user}, and @code{http-x-remote-user}. + +This value is tied to @code{htpasswd-filename} and @code{htpasswd-encryption}." + (sanitizer + (sanitize-delimited-symbols '(none htpasswd remote-user http-x-remote-user) + (current-source-location) + 'type))) + (htpasswd-filename + maybe-file-name + "Path to the htpasswd file. Use htpasswd or similar to generate this file.") + (htpasswd-encryption + maybe-symbol + "Encryption method used in the htpasswd file. Options are @code{plain}, +@code{bcrypt}, and @code{md5}." + (sanitizer + (sanitize-delimited-symbols '(plain bcrypt md5) + (current-source-location) + 'htpasswd-encryption))) + (delay + maybe-non-negative-integer + "Average delay after failed login attempts in seconds.") + (realm + maybe-string + "Message displayed in the client when a password is needed.") + (prefix radicale-)) + +(define-configuration radicale-encoding-configuration + (request + maybe-symbol + "Encoding for responding requests.") + (stock + maybe-symbol + "Encoding for storing local collections.") + (prefix radicale-)) + +(define-configuration radicale-logging-configuration + (level + maybe-symbol + "Set the logging level. One of @code{debug}, @code{info}, @code{warning}, +@code{error}, or @code{critical}." + (sanitizer (sanitize-delimited-symbols '(debug info warning error critical) + (current-source-location) + 'level))) + (mask-passwords? + maybe-boolean + "Whether to include passwords in logs.") + (prefix radicale-)) + +(define-configuration radicale-rights-configuration + (type + maybe-symbol + "Backend used to check collection access rights. The recommended backend is +@code{owner-only}. If access to calendars and address books outside the home +directory of users is granted, clients won't detect these collections and will +not show them to the user. Choosing any other method is only useful if you +access calendars and address books directly via URL. Options are +@code{authenticate}, @code{owner-only}, @code{owner-write}, and +@code{from-file}." + (sanitizer + (sanitize-delimited-symbols '(authenticate owner-only owner-write from-file) + (current-source-location) + 'type))) + (file + maybe-file-name + "File for the rights backend @code{from-file}.") + (prefix radicale-)) + +(define-configuration radicale-server-configuration + (hosts + maybe-comma-separated-ip-list + "List of IP addresses that the server will bind to.") + (max-connections + maybe-non-negative-integer + "Maximum number of parallel connections. Set to 0 to disable the limit.") + (max-content-length + maybe-non-negative-integer + "Maximum size of the request body in byetes.") + (timeout + maybe-non-negative-integer + "Socket timeout in seconds.") + (ssl? + maybe-boolean + "Whether to enable transport layer encryption.") + (certificate + maybe-file-name + "Path of the SSL certificate.") + (key + maybe-file-name + "Path to the private key for SSL. Only effective if @code{ssl?} is +@code{#t}.") + (certificate-authority + maybe-file-name + "Path to CA certificate for validating client certificates. This can be used +to secure TCP traffic between Radicale and a reverse proxy. If you want to +authenticate users with client-side certificates, you also have to write an +authentication plugin that extracts the username from the certificate.") + (prefix radicale-)) + +(define-configuration radicale-storage-configuration + (type + maybe-symbol + "Backend used to store data. Options are @code{multifilesystem} and +@code{multifilesystem-nolock}." + (sanitizer + (sanitize-delimited-symbols '(multifilesystem multifilesystem-nolock) + (current-source-location) + 'type))) + (filesystem-folder + maybe-file-name + "Folder for storing local collections. Created if not present.") + (max-sync-token-age + maybe-non-negative-integer + "Delete sync-tokens that are older than the specified time in seconds.") + (hook + maybe-string + "Command run after changes to storage.") + (prefix radicale-)) + +;; Helpers for using section configurations in the main configuration + +;; XXX These indirections are necessary to avoid creating semantic ambiguity +(define auth-config? radicale-auth-configuration?) +(define encoding-config? radicale-encoding-configuration?) +(define headers-file? file-like?) +(define logging-config? radicale-logging-configuration?) +(define rights-config? radicale-rights-configuration?) +(define server-config? radicale-server-configuration?) +(define storage-config? radicale-storage-configuration?) + +(define-maybe auth-config) +(define-maybe encoding-config) +(define-maybe headers-file) +(define-maybe logging-config) +(define-maybe rights-config) +(define-maybe server-config) +(define-maybe storage-config) + +(define ((serialize-radicale-section fields) name cfg) + #~(format #f "[~a]\n~a\n" '#$name #$(serialize-configuration cfg fields))) + +(define serialize-auth-config + (serialize-radicale-section radicale-auth-configuration-fields)) +(define serialize-encoding-config + (serialize-radicale-section radicale-encoding-configuration-fields)) +(define serialize-logging-config + (serialize-radicale-section radicale-logging-configuration-fields)) +(define serialize-rights-config + (serialize-radicale-section radicale-rights-configuration-fields)) +(define serialize-server-config + (serialize-radicale-section radicale-server-configuration-fields)) +(define serialize-storage-config + (serialize-radicale-section radicale-storage-configuration-fields)) + +(define (serialize-radicale-configuration cfg) + (mixed-text-file + "radicale.conf" + (serialize-configuration cfg radicale-configuration-fields))) + +(define-configuration radicale-configuration + ;; Only fields whose default value does not match upstream are not maybe-types + (package + (file-like radicale) + "Package that provides @command{radicale}.") + (auth + maybe-auth-config + "Configuration for auth-related variables.") + (encoding + maybe-encoding-config + "Configuration for encoding-related variables.") + (headers-file + maybe-headers-file + "Custom HTTP headers." + (serializer + (lambda (field-name value) + #~(begin + (use-modules (ice-9 rdelim)) + (format #f "[headers]\n~a\n\n" + (with-input-from-file #$value read-string)))))) + (logging + maybe-logging-config + "Configuration for logging-related variables.") + (rights + maybe-rights-config + "Configuration for rights-related variables.") + (server + maybe-server-config + "Configuration for server-related variables. Ignored if WSGI is used.") + (storage + maybe-storage-config + "Configuration for storage-related variables.") + (web-interface? + maybe-boolean + "Whether to use Radicale's built-in web interface." + (serializer + (lambda (_ use?) + #~(format #f "[web]\ntype = ~a\n\n" #$(if use? "internal" "none")))))) (define %radicale-accounts (list (user-group @@ -1959,43 +2208,88 @@ (define %radicale-accounts (home-directory "/var/empty") (shell (file-append shadow "/sbin/nologin"))))) -(define radicale-shepherd-service - (match-lambda - (($ package config-file) - (list (shepherd-service - (provision '(radicale)) - (documentation "Run the radicale daemon.") - (requirement '(networking)) - (start #~(make-forkexec-constructor - (list #$(file-append package "/bin/radicale") - "-C" #$config-file) - #:user "radicale" - #:group "radicale")) - (stop #~(make-kill-destructor))))))) +(define (radicale-shepherd-service cfg) + (list (shepherd-service + (provision '(radicale)) + (documentation "Run the radicale daemon.") + (requirement '(networking)) + (start #~(make-forkexec-constructor + (list #$(file-append (radicale-configuration-package cfg) + "/bin/radicale") + "-C" #$(serialize-radicale-configuration cfg)) + #:user "radicale" + #:group "radicale")) + (stop #~(make-kill-destructor))))) (define radicale-activation (match-lambda - (($ package config-file) + (($ _ auth-config _ _ _ _ _ storage-config _) + ;; Get values for the collections directory + ;; See https://radicale.org/v3.html#running-as-a-service + (define filesystem-folder-val + (if (maybe-value-set? storage-config) + (radicale-storage-configuration-filesystem-folder storage-config) + storage-config)) + (define collections-dir + (if (maybe-value-set? filesystem-folder-val) + filesystem-folder-val + "/var/lib/radicale/collections")) + (define collections-parent-dir (dirname collections-dir)) + ;; Get values for the password file directory + (define auth-value-set? (maybe-value-set? auth-config)) + ;; If auth's type is 'none or unset, that means there is no authentication + ;; and we don't need to setup files for it + (define auth? + (and auth-value-set? + (not (eq? (radicale-auth-configuration-type auth-config) 'none)))) + (define password-file-val + (if auth-value-set? + (radicale-auth-configuration-htpasswd-filename auth-config) + auth-config)) + (define password-file-dir + (if (maybe-value-set? password-file-val) + (dirname password-file-val) + "/etc/radicale")) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) - (let ((uid (passwd:uid (getpw "radicale"))) - (gid (group:gid (getgr "radicale")))) - (mkdir-p "/var/lib/radicale/collections") - (chown "/var/lib/radicale" uid gid) - (chown "/var/lib/radicale/collections" uid gid) - (chmod "/var/lib/radicale" #o700))))))) + (let ((user (getpwnam "radicale"))) + ;; Collections directory perms + (mkdir-p/perms #$collections-dir user #o700) + ;; Password file perms + (when #$auth? + ;; In theory, the password file and thus this directory should already + ;; exist because the user has to make them by hand + (mkdir-p/perms #$password-file-dir user #o700)))))))) (define radicale-service-type (service-type (name 'radicale) - (description "Run radicale, a small CalDAV and CardDAV server.") + (description "Run Radicale, a small CalDAV and CardDAV server.") (extensions (list (service-extension shepherd-root-service-type radicale-shepherd-service) (service-extension account-service-type (const %radicale-accounts)) (service-extension activation-service-type radicale-activation))) (default-value (radicale-configuration)))) +(define (generate-radicale-documentation) + (generate-documentation + `((radicale-configuration + ,radicale-configuration-fields + (auth radicale-auth-configuration) + (encoding radicale-encoding-configuration) + (logging radicale-logging-configuration) + (rights radicale-rights-configuration) + (server radicale-server-configuration) + (storage radicale-storage-configuration)) + (radicale-auth-configuration ,radicale-auth-configuration-fields) + (radicale-encoding-configuration ,radicale-encoding-configuration-fields) + (radicale-logging-configuration ,radicale-logging-configuration-fields) + (radicale-rights-configuration ,radicale-rights-configuration-fields) + (radicale-server-configuration ,radicale-server-configuration-fields) + (radicale-storage-configuration ,radicale-storage-configuration-fields)) + 'radicale-configuration)) + ;;; ;;; Rspamd. ;;;