@@ -28248,23 +28248,195 @@ Mail Services
@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
@@ -7,6 +7,7 @@
;;; Copyright © 2020 Jonathan Brielmaier <jonathan.brielmaier@web.de>
;;; Copyright © 2023 Thomas Ieong <th.ieong@free.fr>
;;; Copyright © 2023 Saku Laesvuori <saku@laesvuori.fi>
+;;; Copyright © 2024 Juliana Sims <juli@incana.org>
;;;
;;; 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>
- 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
- (($ <radicale-configuration> 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
- (($ <radicale-configuration> package config-file)
+ (($ <radicale-configuration> _ 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.
;;;