From patchwork Mon Mar 11 02:04:43 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Juliana Sims X-Patchwork-Id: 61617 Return-Path: X-Original-To: patchwork@mira.cbaines.net Delivered-To: patchwork@mira.cbaines.net Received: by mira.cbaines.net (Postfix, from userid 113) id BF15827BBE9; Mon, 11 Mar 2024 02:06:46 +0000 (GMT) X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on mira.cbaines.net X-Spam-Level: X-Spam-Status: No, score=-2.7 required=5.0 tests=BAYES_00,DKIM_INVALID, DKIM_SIGNED,MAILING_LIST_MULTI,SPF_HELO_PASS,URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.6 Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mira.cbaines.net (Postfix) with ESMTPS id 6E9E027BBE2 for ; Mon, 11 Mar 2024 02:06:42 +0000 (GMT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1rjV3e-0001fn-1Z; Sun, 10 Mar 2024 22:06:30 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1rjV3c-0001fJ-IC for guix-patches@gnu.org; Sun, 10 Mar 2024 22:06:28 -0400 Received: from debbugs.gnu.org ([2001:470:142:5::43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1rjV3c-0006nY-A4 for guix-patches@gnu.org; Sun, 10 Mar 2024 22:06:28 -0400 Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1rjV49-0004Ef-Qt for guix-patches@gnu.org; Sun, 10 Mar 2024 22:07:01 -0400 X-Loop: help-debbugs@gnu.org Subject: [bug#69719] [PATCH] services: radicale: Use define-configuration. Resent-From: Juliana Sims Original-Sender: "Debbugs-submit" Resent-CC: guix-patches@gnu.org Resent-Date: Mon, 11 Mar 2024 02:07:01 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: report 69719 X-GNU-PR-Package: guix-patches X-GNU-PR-Keywords: patch To: 69719@debbugs.gnu.org Cc: Juliana Sims X-Debbugs-Original-To: guix-patches@gnu.org Received: via spool by submit@debbugs.gnu.org id=B.171012278316214 (code B ref -1); Mon, 11 Mar 2024 02:07:01 +0000 Received: (at submit) by debbugs.gnu.org; 11 Mar 2024 02:06:23 +0000 Received: from localhost ([127.0.0.1]:38198 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1rjV3V-0004DR-U0 for submit@debbugs.gnu.org; Sun, 10 Mar 2024 22:06:23 -0400 Received: from lists.gnu.org ([209.51.188.17]:40124) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1rjV3S-0004DG-Mp for submit@debbugs.gnu.org; Sun, 10 Mar 2024 22:06:20 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1rjV2r-0001LT-7l for guix-patches@gnu.org; Sun, 10 Mar 2024 22:05:41 -0400 Received: from out-183.mta1.migadu.com ([2001:41d0:203:375::b7]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1rjV2m-0006XF-4S for guix-patches@gnu.org; Sun, 10 Mar 2024 22:05:40 -0400 X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=incana.org; s=key1; t=1710122729; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding; bh=DimVKUODeyXgc6dnzW3bmnm8brvbg7yaOe6miRDHKjs=; b=JQET1lF7BM3BqVbpF3eXpStRFhaXXawcO29mkoB0Ednv8KZhyCvkCT4Mq68LfDZOHiB+WI vuLjwD25N3a6P2idHbtBTYUt2y31PmJpNPxwkY3Hhr9cqxwo938tQE64t03PLgssGMRugu ehqgDgeymDmlwWlyq8UE9JLOfMLuic6VSkvd4E2n1pKjr/1qhww3IIapXD9YWde8Wc60Ea 3/o47GtWA3XsxrpDoIn04FRzCADQhX05Fj+T2a2rPYUjXV1E4nVSq9GA6ecsln/gHoN+Ci EE6eJPx1NuaZJAGdNpoAB851JC6Kdc3IVPs1wlTqhxLt+csDsvGAEDwaPVo36w== Date: Sun, 10 Mar 2024 22:04:43 -0400 Message-ID: <17984bb7ca32629fed475db7e782acd90e538077.1710122683.git.juli@incana.org> MIME-Version: 1.0 X-Migadu-Flow: FLOW_OUT Received-SPF: pass client-ip=2001:41d0:203:375::b7; envelope-from=juli@incana.org; helo=out-183.mta1.migadu.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, SPF_HELO_NONE=0.001, SPF_PASS=-0.001, T_SCC_BODY_TEXT_LINE=-0.01 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: guix-patches@gnu.org List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-to: Juliana Sims X-ACL-Warn: , Juliana Sims via Guix-patches X-Patchwork-Original-From: Juliana Sims via Guix-patches via From: Juliana Sims Errors-To: guix-patches-bounces+patchwork=mira.cbaines.net@gnu.org Sender: guix-patches-bounces+patchwork=mira.cbaines.net@gnu.org X-getmail-retrieved-from-mailbox: Patches Hello, This beast of a patch ports the radicale system service to the define-configuration framework for writing configuration files, instead of simply accepting a handwritten file. It also includes documentation updates (generated with the amazing generate-documentation procedure). The changelog below should cover all changes to all exported symbols, but I'll give an overview of the semantics of what I did and why for the benefit of reviews. Essentially, I took each section of the Radicale configuration file[1] and made it its own define-configuration object (henceforth "configuration"). I made each field using these configurations a maybe type so that the serializer for the configurations could write out the section header iff it was needed. I then made each field of each configuration a maybe type as well so that the default Radicale values are used unless the user overrides them. The logic here is that users will be checking upstream documentation while configuring this service, and we should match upstream's defaults as closely as possible. In the end, the only non-maybe field is the Radicale package itself. This has the mildly annoying effect that a default radicale-service-type produces an empty file on disk as opposed to no file, but the alternative is to query each field individually in a serialization guard which would complicate logic and therefore maintainability. Simplifying maintainibility by providing users with automated checking for configuration values is the primary goal here, and the separate configurations were chosen specifically to avoid the increased cognitive overhead of serialization guards, so this tradeoff feels worth it to me. I also modified the radicale-activation function to query the configurations for the paths of the directories it creates rather than hardcoding the default values. I have tested this code fairly thoroughly, generating system vms that used at least one field of each distinct type, including each section configuration. I've also verified that radicale-activation works as expected and the configuration file is correctly passed to the shepherd service. That said, there's still a good chance I missed something because this is a lot of code. There is one important thing to note about these changes for existing users. Besides removing the option to pass a configuration file verbatim, the defaults of a radicale-service-type have changed to match upstream. Previously the defaults were not in line with upstream. This means users with radicale-service-type and no custom configuration will see silent breakage. I considered adding a news item to alert folks to this, but wasn't sure of the protocol on that so I didn't. I think introducing these breakages is reasonable because the defaults now match upstream and standard filesystem patterns -- /var is not longer being used for static configuration files, for example. I hope folks find this useful. I'm excited to deploy this when it's merged :) Thanks, Juli [1] https://radicale.org/v3.html#configuration * doc/guix.texi (radicale-configuration): Update documentation to reflect new configuration, add new symbols. * gnu/services/mail.scm (%default-radicale-config-file): Delete. (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?): New symbol. (radicale-configuration, radicale-configuration?): Use define-configuration. (radicale-activation, radicale-shepherd-service): Update for new configuration format. (radicale-activation): Use user-defined values for service files. (radicale-service-type): Capitalize "Radicale" in description. --- doc/guix.texi | 194 +++++++++++++++++++++- gnu/services/mail.scm | 368 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 520 insertions(+), 42 deletions(-) base-commit: d084fb4b04a1cebb59959633660013fff495cd0d diff --git a/doc/guix.texi b/doc/guix.texi index 858d5751bfe..096f6bcacdf 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -28252,21 +28252,201 @@ Mail Services server whose value should be a @code{radicale-configuration}. @end defvar +@c %start of fragment + @deftp {Data Type} radicale-configuration -Data type representing the configuration of @command{radicale}. +Data type representing the configuration of @command{radicale}. See the +@uref{https://radicale.org/v3.html#configuration, configuration +documentation} for default values. 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} (type: maybe-auth-config) +Configuration for auth-related variables. This should be a +@code{radicale-auth-configuration}. + +@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} (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}. + +@item @code{htpasswd-filename} (type: maybe-file-name) +Path to the htpasswd file. Use htpasswd or similar to generate this +file. + +@item @code{htpasswd-encryption} (type: maybe-symbol) +Encryption method used in the htpasswd file. Options are @code{plain}, +@code{bcrypt}, and @code{md5}. + +@item @code{delay} (type: maybe-non-negative-integer) +Average delay after failed login attempts in seconds. + +@item @code{realm} (type: maybe-string) +Message displayed in the client when a password is needed. + +@end table + +@end deftp + +@item @code{encoding} (type: maybe-encoding-config) +Configuration for encoding-related variables. This should be a +@code{radicale-encoding-configuration}. + +@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} (type: maybe-symbol) +Encoding for responding requests. + +@item @code{stock} (type: maybe-symbol) +Encoding for storing local collections. @end table + @end deftp +@item @code{headers-file} (type: maybe-text-config) +Custom HTTP headers. This should be a file-like object. + +@item @code{logging} (type: maybe-logging-config) +Configuration for logging-related variables. This should be a +@code{radicale-logging-configuration}. + +@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} (type: maybe-symbol) +Set the logging level. One of @code{debug}, @code{info}, +@code{warning}, @code{error}, or @code{critical}. + +@item @code{mask-passwords?} (type: maybe-boolean) +Whether to include passwords in logs. + +@end table + +@end deftp + +@item @code{rights} (type: maybe-rights-config) +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} (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}. + +@item @code{file} (type: maybe-file-name) +File for the rights backend @code{from-file}. + +@end table + +@end deftp + +@item @code{server} (type: maybe-server-config) +Configuration for server-related variables. Ignored if WSGI is used. +This should be a @code{radicale-server-configuration}. + +@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} (type: maybe-comma-separated-string-list) +List of IP addresses that the server will bind to. + +@item @code{max-connections} (type: maybe-non-negative-integer) +Maximum number of parallel connections. Set to 0 to disable the limit. + +@item @code{max-content-length} (type: maybe-non-negative-integer) +Maximum size of the request body in byetes. + +@item @code{timeout} (type: maybe-non-negative-integer) +Socket timeout in seconds. + +@item @code{ssl?} (type: maybe-boolean) +Whether to enable transport layer encryption. + +@item @code{certificate} (type: maybe-file-name) +Path of the SSL certificate. + +@item @code{key} (type: maybe-file-name) +Path to the private key for SSL. Only effective if @code{ssl?} is +@code{#t}. + +@item @code{certificate-authority} (type: 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. + +@end table + +@end deftp + +@item @code{storage} (type: maybe-storage-config) +Configuration for storage-related variables. This should be a +@code{radicale-storage-configuration}. + +@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} (type: maybe-symbol) +Backend used to store data. Options are @code{multifilesystem} and +@code{multifilesystem-nolock}. + +@item @code{filesystem-folder} (type: maybe-file-name) +Folder for storing local collections. Created if not present. + +@item @code{max-sync-token-age} (type: maybe-non-negative-integer) +Delete sync-tokens that are older than the specified time in seconds. + +@item @code{hook} (type: maybe-string) +Command run after changes to storage. + +@end table + +@end deftp + +@item @code{web-interface?} (type: maybe-boolean) +Whether to use Radicale's built-in web interface. + +@end table + +@end deftp + +@c %end of fragment + @subsubheading Rspamd Service @cindex email @cindex spam diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm index afe1bb60169..2499be87722 100644 --- a/gnu/services/mail.scm +++ b/gnu/services/mail.scm @@ -38,10 +38,12 @@ (define-module (gnu services mail) #:use-module (gnu packages dav) #:use-module (gnu packages tls) #:use-module (guix deprecation) + #:use-module (guix diagnostics) #: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 +81,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 +1942,255 @@ (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-maybe boolean (prefix radicale-)) +(define-maybe comma-separated-string-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-)) +(define-maybe text-config) -[server] -hosts = localhost:5232")) +;; 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-string-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) + (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)) + (serializer radicale-serialize-field)) + (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)) + (serializer radicale-serialize-field)) + (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)) + (serializer radicale-serialize-field)) + (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)) + (serializer radicale-serialize-field)) + (file + maybe-file-name + "File for the rights backend @code{from-file}.") + (prefix radicale-)) + +(define-configuration radicale-server-configuration + (hosts + maybe-comma-separated-string-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)) + (serializer radicale-serialize-field)) + (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 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 logging-config) +(define-maybe rights-config) +(define-maybe server-config) +(define-maybe storage-config) + +(define ((serialize-radicale-section name fields) _ cfg) + #~(format #f "[~a]\n~a\n" #$name #$(serialize-configuration cfg fields))) + +(define serialize-auth-config + (serialize-radicale-section "auth" radicale-auth-configuration-fields)) +(define serialize-encoding-config + (serialize-radicale-section "encoding" radicale-encoding-configuration-fields)) +(define serialize-logging-config + (serialize-radicale-section "logging" radicale-logging-configuration-fields)) +(define serialize-rights-config + (serialize-radicale-section "rights" radicale-rights-configuration-fields)) +(define serialize-server-config + (serialize-radicale-section "server" radicale-server-configuration-fields)) +(define serialize-storage-config + (serialize-radicale-section "storage" 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 + (package radicale) + "Package that provides @command{radicale}.") + (auth + maybe-auth-config + "Configuration for auth-related variables. This should be a +@code{radicale-auth-configuration}.") + (encoding + maybe-encoding-config + "Configuration for encoding-related variables. This should be a +@code{radicale-encoding-configuration}.") + (headers-file + maybe-text-config + "Custom HTTP headers. This should be a file-like object.") + (logging + maybe-logging-config + "Configuration for logging-related variables. This should be a +@code{radicale-logging-configuration}.") + (rights + maybe-rights-config + "Configuration for rights-related variables. This should be a +@code{radicale-rights-configuration}.") + (server + maybe-server-config + "Configuration for server-related variables. Ignored if WSGI is used. This +should be a @code{radicale-server-configuration}.") + (storage + maybe-storage-config + "Configuration for storage-related variables. This should be a +@code{radicale-storage-configuration}.") + (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 +2204,96 @@ (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 + ;; This might be the parent directory for the collections directory, + ;; but we shouldn't assume that. + (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))))))) + ;; Collections directory perms + (mkdir-p #$collections-dir) + (chown #$collections-parent-dir uid gid) + (chown #$collections-dir uid gid) + (chmod #$collections-parent-dir #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 #$password-file-dir) + (chown #$password-file-dir uid gid) + (chmod #$password-file-dir #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. ;;;